From 5e521b832c69b9126d5e9489725f1b02589dc708 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Fri, 27 Jan 2017 15:04:42 -0700 Subject: [PATCH 01/21] add a confirm-email landing page --- .../containers/Configure/CommentSettings.js | 197 ++++++++++-------- client/coral-admin/src/translations.json | 4 + routes/admin/index.js | 5 + routes/api/account/index.js | 4 +- routes/api/users/index.js | 6 +- services/users.js | 10 +- views/admin/confirm-email.ejs | 92 ++++++++ views/email/email-confirm.ejs | 2 +- views/email/email-confirm.txt.ejs | 2 +- 9 files changed, 224 insertions(+), 98 deletions(-) create mode 100644 views/admin/confirm-email.ejs 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/translations.json b/client/coral-admin/src/translations.json index 3718732fe..76749ffd7 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -43,6 +43,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.", @@ -129,6 +131,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/routes/admin/index.js b/routes/admin/index.js index 2a37e8f0f..a3699143e 100644 --- a/routes/admin/index.js +++ b/routes/admin/index.js @@ -1,6 +1,11 @@ const express = require('express'); const router = express.Router(); +// Get /email-confirmation expects a signed JWT in the hash +router.get('/confirm-email', (req, res) => { + res.render('admin/confirm-email'); +}); + // Get /password-reset expects a signed token (JWT) in the hash. // Links to this endpoint are generated by /views/password-reset-email.ejs. router.get('/password-reset', (req, res) => { diff --git a/routes/api/account/index.js b/routes/api/account/index.js index 57647a89d..44749a488 100644 --- a/routes/api/account/index.js +++ b/routes/api/account/index.js @@ -28,8 +28,8 @@ router.post('/email/confirm', (req, res, next) => { UsersService .verifyEmailConfirmation(token) - .then(() => { - res.status(204).end(); + .then(([, referer]) => { + res.json({redirectUri: referer}); }) .catch((err) => { next(err); diff --git a/routes/api/users/index.js b/routes/api/users/index.js index 3ea3f3213..d42c5039b 100644 --- a/routes/api/users/index.js +++ b/routes/api/users/index.js @@ -69,8 +69,8 @@ router.post('/:user_id/status', authorization.needed('ADMIN'), (req, res, next) * @param {String} userID the id for the user to send the email to * @param {String} email the email for the user to send the email to */ -const SendEmailConfirmation = (app, userID, email) => UsersService - .createEmailConfirmToken(userID, email) +const SendEmailConfirmation = (app, userID, email, referer) => UsersService + .createEmailConfirmToken(userID, email, referer) .then((token) => { return mailer.sendSimple({ app, // needed to render the templates. @@ -103,7 +103,7 @@ router.post('/', (req, res, next) => { if (requireEmailConfirmation) { - SendEmailConfirmation(req.app, user.id, email) + SendEmailConfirmation(req.app, user.id, email, req.header('Referer')) .then(() => { // Then send back the user. diff --git a/services/users.js b/services/users.js index 0bf22e9e0..1a2c384d6 100644 --- a/services/users.js +++ b/services/users.js @@ -531,7 +531,7 @@ module.exports = class UsersService { * @param {String} email The email that we are needing to get confirmed. * @return {Promise} */ - static createEmailConfirmToken(userID, email) { + static createEmailConfirmToken(userID, email, referer) { if (!email || typeof email !== 'string') { return Promise.reject('email is required when creating a JWT for resetting passord'); } @@ -555,6 +555,7 @@ module.exports = class UsersService { const payload = { email, + referer, userID }; @@ -579,9 +580,9 @@ module.exports = class UsersService { .verifyToken(token, { subject: EMAIL_CONFIRM_JWT_SUBJECT }) - .then(({userID, email}) => { + .then(({userID, email, referer}) => { - return UserModel + const userUpdate = UserModel .update({ id: userID, profiles: { @@ -595,7 +596,10 @@ module.exports = class UsersService { 'profiles.$.metadata.confirmed_at': new Date() } }); + + return Promise.all([userUpdate, referer]); }); + } }; diff --git a/views/admin/confirm-email.ejs b/views/admin/confirm-email.ejs new file mode 100644 index 000000000..7d0475325 --- /dev/null +++ b/views/admin/confirm-email.ejs @@ -0,0 +1,92 @@ + + + + + + Confirm Email + + + + + + +
+
+
+

Confirm Email Address

+
+
+ Click the button below to confirm your new user account. +
+ +
+ +
+ + + + + diff --git a/views/email/email-confirm.ejs b/views/email/email-confirm.ejs index da1b0e815..2644c2225 100644 --- a/views/email/email-confirm.ejs +++ b/views/email/email-confirm.ejs @@ -1,3 +1,3 @@

A email confirmation has been requested for the following account: <%= email %>.

-

To confirm the account, please visit the following link: http://example.com/email/confirm/endpoint#<%= token %>

+

To confirm the account, please visit the following link: http://example.com/email/confirm/endpoint#<%= token %>

If you did not request this, you can safely ignore this email.

diff --git a/views/email/email-confirm.txt.ejs b/views/email/email-confirm.txt.ejs index 764991d8d..4c7d7d312 100644 --- a/views/email/email-confirm.txt.ejs +++ b/views/email/email-confirm.txt.ejs @@ -4,6 +4,6 @@ A email confirmation has been requested for the following account: To confirm the account, please visit the following link: - http://example.com/email/confirm/endpoint#<%= token %> + <%= rootURL %>/confirm/endpoint#<%= token %> If you did not request this, you can safely ignore this email. From 235214642e20b9adadfbe59180e3b4ea7ff6e3cb Mon Sep 17 00:00:00 2001 From: David Jay Date: Mon, 30 Jan 2017 17:55:06 -0500 Subject: [PATCH 02/21] Fixing bug where timeAgo cannot interpret dates from mongoose properly. --- client/coral-framework/modules/i18n/i18n.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)); }; } } From 976c821715e58885d581e0ae5ab901981a49784a Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Mon, 30 Jan 2017 16:00:24 -0700 Subject: [PATCH 03/21] move FormField to coral-ui --- client/coral-embed-stream/src/Embed.js | 3 +- client/coral-framework/actions/auth.js | 5 ++ .../coral-sign-in/components/SignInContent.js | 4 +- .../coral-sign-in/components/SignUpContent.js | 6 +-- client/coral-sign-in/components/styles.css | 34 +++++-------- client/coral-sign-in/translations.js | 4 ++ client/coral-ui/components/FormField.css | 48 +++++++++++++++++++ .../components/FormField.js | 12 ++++- client/coral-ui/index.js | 2 + routes/api/users/index.js | 6 ++- 10 files changed, 91 insertions(+), 33 deletions(-) create mode 100644 client/coral-ui/components/FormField.css rename client/{coral-sign-in => coral-ui}/components/FormField.js (67%) diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index 5922266fa..3318b8d63 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'; import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui'; -const {logout, showSignInDialog} = authActions; +const {logout, showSignInDialog, requestConfirmEmail} = authActions; const {addNotification, clearNotification} = notificationActions; const {fetchAssetSuccess} = assetActions; @@ -188,6 +188,7 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ + requestConfirmEmail: () => dispatch(requestConfirmEmail()), loadAsset: (asset) => dispatch(fetchAssetSuccess(asset)), addNotification: (type, text) => dispatch(addNotification(type, text)), clearNotification: () => dispatch(clearNotification()), diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index e815fd5d0..e71635ae1 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -137,3 +137,8 @@ export const checkLogin = () => dispatch => { }) .catch(error => dispatch(checkLoginFailure(error))); }; + +export const requestConfirmEmail = () => dispatch => { + +}; + diff --git a/client/coral-sign-in/components/SignInContent.js b/client/coral-sign-in/components/SignInContent.js index de5e77a49..efd6ad5c2 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -1,8 +1,6 @@ import React from 'react'; -import Button from 'coral-ui/components/Button'; -import FormField from './FormField'; import Alert from './Alert'; -import Spinner from 'coral-ui/components/Spinner'; +import {Button, FormField, Spinner} 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/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js index 4144b1d2f..91bbeec39 100644 --- a/client/coral-sign-in/components/SignUpContent.js +++ b/client/coral-sign-in/components/SignUpContent.js @@ -1,9 +1,7 @@ import React from 'react'; -import FormField from './FormField'; +// 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 f1a7bace3..08146f73f 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; @@ -150,3 +128,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/translations.js b/client/coral-sign-in/translations.js index d4f5208bd..3dd3171f1 100644 --- a/client/coral-sign-in/translations.js +++ b/client/coral-sign-in/translations.js @@ -1,6 +1,8 @@ export default { en: { 'signIn': { + emailConfirmCTA: 'Please verify your email address. We sent an email to {0} for verification.', + requestNewConfirmEmail: 'Request another email', notYou: 'Not you?', loggedInAs: 'Logged in as', facebookSignIn: 'Sign in with Facebook', @@ -28,6 +30,8 @@ export default { }, es: { 'signIn': { + emailConfirmCTA: 'Por favor verifique su correo electronico. Le enviamos un correo a {0} para verificar.', + requestNewConfirmEmail: 'Enviar otro correo', notYou: 'No eres tu?', loggedInAs: 'Entraste como', facebookSignIn: 'Entrar con Facebook', diff --git a/client/coral-ui/components/FormField.css b/client/coral-ui/components/FormField.css new file mode 100644 index 000000000..bafbbbebf --- /dev/null +++ b/client/coral-ui/components/FormField.css @@ -0,0 +1,48 @@ +.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; +} + +input.error{ + border: solid 2px #f44336; +} + +.errorMsg, .hint { + color: grey; + font-weight: 600; + padding: 3px 0 16px; +} + +.attention { + display: inline-block; + width: 15px; + height: 15px; + background: #B71C1C; + color: #FFEBEE; + font-weight: bolder; + padding: 4px; + vertical-align: middle; + border-radius: 20px; + box-sizing: border-box; + font-size: 9px; + line-height: 7px; + text-align: center; + margin-right: 5px; +} diff --git a/client/coral-sign-in/components/FormField.js b/client/coral-ui/components/FormField.js similarity index 67% rename from client/coral-sign-in/components/FormField.js rename to client/coral-ui/components/FormField.js index ceb2d939e..60c4867f9 100644 --- a/client/coral-sign-in/components/FormField.js +++ b/client/coral-ui/components/FormField.js @@ -1,5 +1,5 @@ -import React from 'react'; -import styles from './styles.css'; +import React, {PropTypes} from 'react'; +import styles from './FormField.css'; const FormField = ({className, showErrors = false, errorMsg, label, ...props}) => (
@@ -15,4 +15,12 @@ const FormField = ({className, showErrors = false, errorMsg, label, ...props}) =
); +FormField.propTypes = { + label: PropTypes.string, + value: PropTypes.string, + onChange: PropTypes.func, + errorMsg: PropTypes.string, + type: PropTypes.string +}; + export default FormField; diff --git a/client/coral-ui/index.js b/client/coral-ui/index.js index 23d5d876a..4283422c9 100644 --- a/client/coral-ui/index.js +++ b/client/coral-ui/index.js @@ -13,4 +13,6 @@ export {default as Icon} from './components/Icon'; export {default as List} from './components/List'; export {default as Item} from './components/Item'; export {default as Card} from './components/Card'; +export {default as FormField} from './components/FormField'; +export {default as Success} from './components/Success'; export {default as Pager} from './components/Pager'; diff --git a/routes/api/users/index.js b/routes/api/users/index.js index d42c5039b..563acd2a1 100644 --- a/routes/api/users/index.js +++ b/routes/api/users/index.js @@ -6,6 +6,7 @@ const CommentsService = require('../../../services/comments'); const mailer = require('../../../services/mailer'); const authorization = require('../../../middleware/authorization'); +// get a list of users. router.get('/', authorization.needed('ADMIN'), (req, res, next) => { const { value = '', @@ -44,6 +45,7 @@ router.post('/:user_id/role', authorization.needed('ADMIN'), (req, res, next) => .catch(next); }); +// update the status of a user router.post('/:user_id/status', authorization.needed('ADMIN'), (req, res, next) => { UsersService .setStatus(req.params.user_id, req.body.status) @@ -85,10 +87,12 @@ const SendEmailConfirmation = (app, userID, email, referer) => UsersService }); }); +// create a local user. router.post('/', (req, res, next) => { const { email, password, + redirectUri, displayName } = req.body; @@ -103,7 +107,7 @@ router.post('/', (req, res, next) => { if (requireEmailConfirmation) { - SendEmailConfirmation(req.app, user.id, email, req.header('Referer')) + SendEmailConfirmation(req.app, user.id, email, redirectUri) .then(() => { // Then send back the user. From e8d8c7af5b8bfd36b65fb46752b2fd80d7cfd1e3 Mon Sep 17 00:00:00 2001 From: David Jay Date: Mon, 30 Jan 2017 18:04:17 -0500 Subject: [PATCH 04/21] Switching e2e to port 3011. --- nightwatch.conf.js | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nightwatch.conf.js b/nightwatch.conf.js index 2e0cc5020..b2a2b9baa 100644 --- a/nightwatch.conf.js +++ b/nightwatch.conf.js @@ -24,7 +24,7 @@ module.exports = { }, 'test_settings': { 'default': { - 'launch_url' : 'http://localhost:3000', + 'launch_url' : 'http://localhost:3011', 'selenium_port': 6666, 'selenium_host': 'localhost', 'silent': true, @@ -48,7 +48,7 @@ module.exports = { ] }, 'integration': { - 'launch_url': 'http://localhost:3000' + 'launch_url': 'http://localhost:3011' } } }; diff --git a/package.json b/package.json index 641d85757..b597413d2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "lint-fix": "eslint bin/* . --fix", "test": "TEST_MODE=unit NODE_ENV=test mocha -R ${NPM_PACKAGE_CONFIG_MOCHA_REPORTER:-spec}", "test-cover": "TEST_MODE=unit NODE_ENV=test istanbul cover _mocha --report text --check-coverage -- -R spec", - "pree2e": "NODE_ENV=test scripts/pree2e.sh", + "pree2e": "NODE_ENV=test TALK_PORT=3011 scripts/pree2e.sh", "e2e": "NODE_ENV=test nightwatch", "poste2e": "NODE_ENV=test scripts/poste2e.sh", "embed-start": "NODE_ENV=development npm run build && ./bin/cli serve --jobs", From e6ef4b615f15cd76476f7cd3329b009dcb716bf9 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 30 Jan 2017 17:46:09 -0700 Subject: [PATCH 05/21] Changed prompt lib, updated cli's - fixes #252 --- bin/cli | 36 +------ bin/cli-assets | 6 +- bin/cli-jobs | 2 +- bin/cli-serve | 7 +- bin/cli-settings | 51 ---------- bin/cli-setup | 82 ++++++++++++++++ bin/cli-users | 227 ++++++++++++++++++++++--------------------- bin/commander.js | 46 +++++++++ errors.js | 5 + models/setting.js | 14 ++- package.json | 3 +- services/settings.js | 11 ++- services/wordlist.js | 1 + util.js | 4 +- 14 files changed, 282 insertions(+), 213 deletions(-) delete mode 100755 bin/cli-settings create mode 100755 bin/cli-setup create mode 100644 bin/commander.js 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); From 91cd2e9e86cbea8aa5682e939beb1274da79df90 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 30 Jan 2017 18:21:04 -0700 Subject: [PATCH 06/21] Fixes #266 --- bin/cli-serve | 19 ++++++------------- bin/cli-setup | 19 ++++++++++--------- bin/cli-users | 28 +++++++++++++++++++++++----- init.js | 13 ------------- models/setting.js | 18 ++++++++++-------- services/settings.js | 15 +++++++-------- services/users.js | 25 ++++++++++++++++--------- test/routes/api/auth/index.js | 4 ++-- 8 files changed, 74 insertions(+), 67 deletions(-) delete mode 100644 init.js diff --git a/bin/cli-serve b/bin/cli-serve index d861d8c5d..0427dcf34 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -4,7 +4,6 @@ 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'); @@ -87,19 +86,13 @@ 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); } //============================================================================== diff --git a/bin/cli-setup b/bin/cli-setup index 1c348d212..7645e171d 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -31,20 +31,21 @@ program console.log('We\'ll ask you some questions in order to setup your installation of Talk.\n'); SettingsService - .retrieve() - .catch(() => new SettingModel()) + .init() .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 + default: settings.organizationName, + validate: (input) => { + if (input && input.length > 0) { + return true; + } + + return 'Organization Name is required.'; + } }, { type: 'list', @@ -69,7 +70,7 @@ SettingsService } }); - return SettingsService.update(settings); + return settings.save(); }); }) .then(() => { diff --git a/bin/cli-users b/bin/cli-users index 3ce418927..ab89626eb 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -12,8 +12,8 @@ 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) { +const validateRequired = (msg = 'Field is required', len = 1) => (input) => { + if (input && input.length >= len) { return true; } @@ -53,18 +53,36 @@ function getUserCreateAnswers(options) { name: 'password', message: 'Password', type: 'password', - validate: validateRequired('Password is required') + filter: (password) => { + return UsersService + .isValidPassword(password) + .catch((err) => { + throw err.message; + }); + } }, { name: 'confirmPassword', message: 'Confirm Password', type: 'password', - validate: validateRequired('Confirm Password is required') + filter: (confirmPassword) => { + return UsersService + .isValidPassword(confirmPassword) + .catch((err) => { + throw err.message; + }); + } }, { name: 'displayName', message: 'Display Name', - validate: validateRequired('Display Name is required') + filter: (displayName) => { + return UsersService + .isValidDisplayName(displayName) + .catch((err) => { + throw err.message; + }); + } }, { name: 'roles', diff --git a/init.js b/init.js deleted file mode 100644 index cd0c0bbc9..000000000 --- a/init.js +++ /dev/null @@ -1,13 +0,0 @@ -const SettingsService = require('./services/settings'); - -module.exports = () => Promise.all([ - - // Upsert the settings object. - SettingsService.init({ - moderation: 'POST', - wordlist: { - banned: [], - suspect: [] - } - }) -]); diff --git a/models/setting.js b/models/setting.js index 26faf1319..25882b3f8 100644 --- a/models/setting.js +++ b/models/setting.js @@ -6,13 +6,6 @@ const MODERATION_OPTIONS = [ 'POST' ]; -const WordlistSchema = new Schema({ - banned: [String], - suspect: [String] -}, { - _id: false -}); - /** * SettingSchema manages application settings that get used on front and backend. * @type {Schema} @@ -48,7 +41,16 @@ const SettingSchema = new Schema({ type: String, default: 'Expired' }, - wordlist: WordlistSchema, + wordlist: { + banned: { + type: Array, + default: [] + }, + suspect: { + type: Array, + default: [] + } + }, charCount: { type: Number, default: 5000 diff --git a/services/settings.js b/services/settings.js index 5d4133fe4..2bb15319d 100644 --- a/services/settings.js +++ b/services/settings.js @@ -44,15 +44,14 @@ module.exports = class SettingsService { /** * This is run once when the app starts to ensure settings are populated. - * @return {Promise} null initialize the global settings object */ - static init(defaults) { + static init(defaults = {}) { + return SettingsService + .retrieve() + .catch(() => { + let settings = new SettingModel(defaults); - // Inject the defaults on top of the passed in defaults to ensure that the new - // settings conform to the required selector. - defaults = Object.assign({}, defaults, selector); - - // Actually update the settings collection. - return SettingsService.update(defaults); + return settings.save(); + }); } }; diff --git a/services/users.js b/services/users.js index 7a888ede1..635e39e47 100644 --- a/services/users.js +++ b/services/users.js @@ -189,6 +189,18 @@ module.exports = class UsersService { return Wordlist.displayNameCheck(displayName); } + static isValidPassword(password) { + if (!password) { + return Promise.reject(errors.ErrMissingPassword); + } + + if (password.length < 8) { + return Promise.reject(errors.ErrPasswordTooShort); + } + + return Promise.resolve(password); + } + /** * Creates the local user with a given email, password, and name. * @param {String} email email of the new user @@ -205,15 +217,10 @@ module.exports = class UsersService { email = email.toLowerCase().trim(); displayName = displayName.toLowerCase().trim(); - if (!password) { - return Promise.reject(errors.ErrMissingPassword); - } - - if (password.length < 8) { - return Promise.reject(errors.ErrPasswordTooShort); - } - - return UsersService.isValidDisplayName(displayName) + return Promise.all([ + UsersService.isValidDisplayName(displayName), + UsersService.isValidPassword(password) + ]) .then(() => { // displayName is valid return new Promise((resolve, reject) => { bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => { diff --git a/test/routes/api/auth/index.js b/test/routes/api/auth/index.js index 4882d859e..1bf8f4992 100644 --- a/test/routes/api/auth/index.js +++ b/test/routes/api/auth/index.js @@ -66,7 +66,7 @@ describe('/api/v1/auth/local', () => { describe('email confirmation enabled', () => { - beforeEach(() => SettingsService.init({requireEmailConfirmation: true})); + beforeEach(() => SettingsService.update({requireEmailConfirmation: true})); describe('#post', () => { it('should not allow a login from a user that is not confirmed', () => { @@ -74,7 +74,7 @@ describe('/api/v1/auth/local', () => { .post('/api/v1/auth/local') .send({email: 'maria@gmail.com', password: 'password!'}) .catch((err) => { - err.response.should.have.status(401); + expect(err).to.have.status(401); return UsersService.createEmailConfirmToken(mockUser.id, mockUser.profiles[0].id); }) From 864413b5d6ed99fbceefd07b36a0e19edb5be2b3 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 30 Jan 2017 18:38:06 -0700 Subject: [PATCH 07/21] Fixed issues with e2e --- bin/cli-setup | 11 ++++++++--- bin/cli-users | 21 +++++++++++---------- scripts/pree2e.sh | 4 ++-- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/bin/cli-setup b/bin/cli-setup index 7645e171d..eaa2b90b1 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -22,17 +22,22 @@ util.onshutdown([ 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 //============================================================================== -console.log('We\'ll ask you some questions in order to setup your installation of Talk.\n'); - 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', @@ -74,7 +79,7 @@ SettingsService }); }) .then(() => { - console.log('\nTalk is now installed!'); + console.log('Talk is now installed!'); util.shutdown(); }) .catch((err) => { diff --git a/bin/cli-users b/bin/cli-users index ab89626eb..072a143ee 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -28,18 +28,19 @@ util.onshutdown([ 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) { - - let roles = {}; - roles[options.role] = true; - - return Promise.resolve({ - email: options.email, - password: options.password, - displayName: options.name, - roles - }); + user.roles = [options.role]; } + + return Promise.resolve(user); } return inquirer.prompt([ diff --git a/scripts/pree2e.sh b/scripts/pree2e.sh index 6210c09f0..9ed5b7b89 100755 --- a/scripts/pree2e.sh +++ b/scripts/pree2e.sh @@ -3,8 +3,8 @@ # install selenium selenium-standalone install --config=./selenium.config.js -#Init settings -./bin/cli settings init +# Init application +./bin/cli setup --defaults # Creating Admin Test User ./bin/cli users create --flag_mode --email "admin@test.com" --password "testtest" --name "AdminTestUser" --role "ADMIN" From 0af41a5b6baafdd350d01a1ec9dabf963622a2e2 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 30 Jan 2017 18:41:23 -0700 Subject: [PATCH 08/21] Updated circle test --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From 56ddc493869b850daffa88c24f886cbb4d606724 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 31 Jan 2017 12:04:19 -0700 Subject: [PATCH 09/21] resend verify email --- client/coral-framework/actions/auth.js | 31 +++- client/coral-framework/constants/auth.js | 5 + client/coral-framework/helpers/response.js | 14 +- client/coral-framework/reducers/auth.js | 4 + client/coral-framework/translations.json | 2 + .../coral-sign-in/components/SignInContent.js | 137 +++++++++++------- .../containers/SignInContainer.js | 16 ++ client/coral-sign-in/translations.js | 8 +- errors.js | 20 +++ routes/api/users/index.js | 20 +++ services/passport.js | 5 +- services/users.js | 60 ++++---- test/routes/api/auth/index.js | 3 + 13 files changed, 233 insertions(+), 92 deletions(-) diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index e71635ae1..d06abaee0 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -22,6 +22,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()); @@ -32,7 +33,18 @@ export const fetchSignIn = (formData) => (dispatch) => { dispatch(hideSignInDialog()); dispatch(addItem(user, 'users')); }) - .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 @@ -138,7 +150,20 @@ export const checkLogin = () => dispatch => { .catch(error => dispatch(checkLoginFailure(error))); }; -export const requestConfirmEmail = () => dispatch => { +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()); + 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/reducers/auth.js b/client/coral-framework/reducers/auth.js index 36f8e0764..54ffa37a6 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -11,6 +11,7 @@ const initialState = Map({ error: '', passwordRequestSuccess: null, passwordRequestFailure: null, + emailConfirmationFailure: false, successSignUp: false }); @@ -33,6 +34,7 @@ export default function auth (state = initialState, action) { error: '', passwordRequestFailure: null, passwordRequestSuccess: null, + emailConfirmationFailure: false, successSignUp: false })); case actions.CHANGE_VIEW : @@ -101,6 +103,8 @@ 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); 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/SignInContent.js b/client/coral-sign-in/components/SignInContent.js index efd6ad5c2..d3d064c06 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {PropTypes} from 'react'; import Alert from './Alert'; import {Button, FormField, Spinner} from 'coral-ui'; import styles from './styles.css'; @@ -6,60 +6,89 @@ 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, + 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')}

+ + + + :
+ + +
+ { + !props.auth.isLoading ? + + : + + } +
+ + } + - - -
-); + ); +}; + +SignInContent.propTypes = { + handleResendConfirmation: PropTypes.func.isRequired, + handleChangeEmail: PropTypes.func.isRequired, + emailToBeResent: PropTypes.string.isRequired +}; export default SignInContent; diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 07f82a849..5cf07d605 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,6 +40,8 @@ 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); @@ -71,6 +75,17 @@ class SignInContainer extends Component { }); } + handleChangeEmail(e) { + const {value} = e.target; + console.log('handleChangeEmail', value); + this.setState({emailToBeResent: value}); + } + + handleResendConfirmation(e) { + e.preventDefault(); + this.props.requestConfirmEmail(this.state.emailToBeResent); + } + addError(name, error) { return this.setState(state => ({ errors: { @@ -159,6 +174,7 @@ const mapDispatchToProps = dispatch => ({ fetchSignIn: formData => dispatch(fetchSignIn(formData)), fetchSignInFacebook: () => dispatch(fetchSignInFacebook()), fetchForgotPassword: formData => dispatch(fetchForgotPassword(formData)), + requestConfirmEmail: email => dispatch(requestConfirmEmail(email)), showSignInDialog: () => dispatch(showSignInDialog()), changeView: view => dispatch(changeView(view)), handleClose: () => dispatch(hideSignInDialog()), diff --git a/client/coral-sign-in/translations.js b/client/coral-sign-in/translations.js index 3dd3171f1..137141d34 100644 --- a/client/coral-sign-in/translations.js +++ b/client/coral-sign-in/translations.js @@ -1,8 +1,8 @@ export default { en: { 'signIn': { - emailConfirmCTA: 'Please verify your email address. We sent an email to {0} for verification.', - requestNewConfirmEmail: 'Request another email', + emailConfirmCTA: 'Please verify your email address.', + requestNewConfirmEmail: 'Request another email:', notYou: 'Not you?', loggedInAs: 'Logged in as', facebookSignIn: 'Sign in with Facebook', @@ -30,8 +30,8 @@ export default { }, es: { 'signIn': { - emailConfirmCTA: 'Por favor verifique su correo electronico. Le enviamos un correo a {0} para verificar.', - requestNewConfirmEmail: 'Enviar otro correo', + emailConfirmCTA: 'Por favor verifique su correo electronico.', + requestNewConfirmEmail: 'Enviar otro correo:', notYou: 'No eres tu?', loggedInAs: 'Entraste como', facebookSignIn: 'Entrar con Facebook', diff --git a/errors.js b/errors.js index 1b26e7170..39c0cb8fc 100644 --- a/errors.js +++ b/errors.js @@ -89,6 +89,20 @@ class ErrAssetCommentingClosed extends APIError { } } +/** + * ErrAuthentication is returned when there is an error authenticating and the + * message is provided. + */ +class ErrAuthentication extends APIError { + constructor(message = null) { + super('authentication error occured', { + status: 401 + }, { + message + }); + } +} + // ErrContainsProfanity is returned in the event that the middleware detects // profanity/wordlisted words in the payload. const ErrContainsProfanity = new APIError('Suspected profanity. If you think this in error, please let us know!', { @@ -110,6 +124,10 @@ const ErrNotAuthorized = new APIError('not authorized', { status: 401 }); +// ErrSettingsNotInit is returned when the settings object isn't available in +// the mongo collection. +const ErrSettingsNotInit = new Error('settings not initialized, run `./bin/cli settings init` to setup the settings'); + module.exports = { ExtendableError, APIError, @@ -125,5 +143,7 @@ module.exports = { ErrAssetCommentingClosed, ErrNotFound, ErrInvalidAssetURL, + ErrSettingsNotInit, + ErrAuthentication, ErrNotAuthorized }; diff --git a/routes/api/users/index.js b/routes/api/users/index.js index 563acd2a1..01b34ecd8 100644 --- a/routes/api/users/index.js +++ b/routes/api/users/index.js @@ -4,6 +4,7 @@ const UsersService = require('../../../services/users'); const SettingsService = require('../../../services/settings'); const CommentsService = require('../../../services/comments'); const mailer = require('../../../services/mailer'); +const errors = require('../../../errors'); const authorization = require('../../../middleware/authorization'); // get a list of users. @@ -142,6 +143,25 @@ router.post('/:user_id/actions', authorization.needed(), (req, res, next) => { }); }); +// trigger an email confirmation re-send by a new user +router.post('/resend-confirm', (req, res, next) => { + const {email} = req.body; + + if (!email) { + return next(errors.ErrMissingEmail); + } + + // find user by email. + // if the local profile is verified, return an error code? + // send a 204 after the email is re-sent + SendEmailConfirmation(req.app, null, email, process.env.TALK_ROOT_URL) + .then(() => { + res.status(204).end(); + }) + .catch(next); +}); + +// trigger an email confirmation re-send from the admin panel router.post('/:user_id/email/confirm', authorization.needed('ADMIN'), (req, res, next) => { const { user_id diff --git a/services/passport.js b/services/passport.js index f24d12165..dcb6904ec 100644 --- a/services/passport.js +++ b/services/passport.js @@ -3,6 +3,7 @@ const UsersService = require('./users'); const SettingsService = require('./settings'); const LocalStrategy = require('passport-local').Strategy; const FacebookStrategy = require('passport-facebook').Strategy; +const errors = require('../errors'); //============================================================================== // SESSION SERIALIZATION @@ -34,7 +35,7 @@ function ValidateUserLogin(loginProfile, user, done) { } if (user.disabled) { - return done(null, false, {message: 'Account disabled'}); + return done(new errors.ErrAuthentication('Account disabled')); } // If the user isn't a local user (i.e., a social user). @@ -61,7 +62,7 @@ function ValidateUserLogin(loginProfile, user, done) { // If the profile doesn't have a metadata field, or it does not have a // confirmed_at field, or that field is null, then send them back. if (!profile.metadata || !profile.metadata.confirmed_at || profile.metadata.confirmed_at === null) { - return done(null, false, {message: `Email address ${loginProfile.id} not verified.`}); + return done(new errors.ErrAuthentication(loginProfile.id)); } } diff --git a/services/users.js b/services/users.js index 1a2c384d6..0fc16f366 100644 --- a/services/users.js +++ b/services/users.js @@ -531,41 +531,51 @@ module.exports = class UsersService { * @param {String} email The email that we are needing to get confirmed. * @return {Promise} */ - static createEmailConfirmToken(userID, email, referer) { + static createEmailConfirmToken(userID = null, email, referer) { if (!email || typeof email !== 'string') { return Promise.reject('email is required when creating a JWT for resetting passord'); } + const tokenOptions = { + jwtid: uuid.v4(), + algorithm: 'HS256', + expiresIn: '1d', + subject: EMAIL_CONFIRM_JWT_SUBJECT + }; + email = email.toLowerCase(); + let userPromise; - return UsersService - .findById(userID) - .then((user) => { - if (!user) { - return Promise.reject(new Error('user not found')); - } + if (!userID) { - // Get the profile representing the local account. - let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local'); + // if there is no userID, we're coming from the endpoint where a new user is re-requesting a confirmation email + // and we don't know the userID + userPromise = UserModel.findOne({profiles: {$elemMatch: {id: email, provider: 'local'}}}); + } else { + userPromise = UsersService.findById(userID); + } - // Ensure that the user email hasn't already been verified. - if (profile && profile.metadata && profile.metadata.confirmed_at) { - return Promise.reject(new Error('email address already confirmed')); - } + return userPromise.then((user) => { + if (!user) { + return Promise.reject(new Error('user not found')); + } - const payload = { - email, - referer, - userID - }; + // Get the profile representing the local account. + let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local'); - return jwt.sign(payload, process.env.TALK_SESSION_SECRET, { - jwtid: uuid.v4(), - algorithm: 'HS256', - expiresIn: '1d', - subject: EMAIL_CONFIRM_JWT_SUBJECT - }); - }); + // Ensure that the user email hasn't already been verified. + if (profile && profile.metadata && profile.metadata.confirmed_at) { + return Promise.reject(new Error('email address already confirmed')); + } + + const payload = { + email, + referer, + userID: user.id + }; + + return jwt.sign(payload, process.env.TALK_SESSION_SECRET, tokenOptions); + }); } /** diff --git a/test/routes/api/auth/index.js b/test/routes/api/auth/index.js index 4882d859e..60efc30a2 100644 --- a/test/routes/api/auth/index.js +++ b/test/routes/api/auth/index.js @@ -75,6 +75,9 @@ describe('/api/v1/auth/local', () => { .send({email: 'maria@gmail.com', password: 'password!'}) .catch((err) => { err.response.should.have.status(401); + err.response.body.should.have.property('error'); + err.response.body.error.should.have.property('metadata'); + err.response.body.error.metadata.should.have.property('message', 'maria@gmail.com'); return UsersService.createEmailConfirmToken(mockUser.id, mockUser.profiles[0].id); }) From 3d83c246f7cfee0cae8f7bf8ca7ee7ec43baa972 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 31 Jan 2017 12:08:59 -0700 Subject: [PATCH 10/21] simplify email text --- views/email/email-confirm.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/email/email-confirm.ejs b/views/email/email-confirm.ejs index 2644c2225..dd2397edf 100644 --- a/views/email/email-confirm.ejs +++ b/views/email/email-confirm.ejs @@ -1,3 +1,3 @@

A email confirmation has been requested for the following account: <%= email %>.

-

To confirm the account, please visit the following link: http://example.com/email/confirm/endpoint#<%= token %>

+

To confirm the account, please visit the following link: Confirm Email

If you did not request this, you can safely ignore this email.

From ec1dfefc5d8da84a4779a0ce54ec1bd4880a2a2f Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 31 Jan 2017 12:15:33 -0700 Subject: [PATCH 11/21] lint --- client/coral-sign-in/components/SignUpContent.js | 1 - errors.js | 1 - 2 files changed, 2 deletions(-) diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js index 91bbeec39..246d9d884 100644 --- a/client/coral-sign-in/components/SignUpContent.js +++ b/client/coral-sign-in/components/SignUpContent.js @@ -1,5 +1,4 @@ import React from 'react'; -// import FormField from './FormField'; import Alert from './Alert'; import {Button, FormField, Spinner, Success} from 'coral-ui'; import styles from './styles.css'; diff --git a/errors.js b/errors.js index 12b94d00b..08a5c3ae2 100644 --- a/errors.js +++ b/errors.js @@ -144,7 +144,6 @@ module.exports = { ErrAssetCommentingClosed, ErrNotFound, ErrInvalidAssetURL, - ErrSettingsNotInit, ErrAuthentication, ErrNotAuthorized }; From 3ca263bcfbf4b8aa0fdbc9b81c35de613d1ff1a1 Mon Sep 17 00:00:00 2001 From: David Jay Date: Tue, 31 Jan 2017 14:23:15 -0500 Subject: [PATCH 12/21] Updating mock article template to support mobile widths. --- views/article.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/article.ejs b/views/article.ejs index d0b468cf2..b8c717d1d 100644 --- a/views/article.ejs +++ b/views/article.ejs @@ -12,7 +12,7 @@ main { margin-left:auto; margin-right:auto; - width:500px; + min-width:500px; } <%= title %> From 7babb14bab91593a4e5dac269a69877c1355e246 Mon Sep 17 00:00:00 2001 From: David Jay Date: Tue, 31 Jan 2017 14:37:52 -0500 Subject: [PATCH 13/21] Addressing bug in asset search. --- client/coral-admin/src/containers/Streams/Streams.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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; }); From a13b1c95d65ee15e8e9e4f8b9dd795248b3efc8d Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 31 Jan 2017 12:57:09 -0700 Subject: [PATCH 14/21] Adjusted form --- routes/api/account/index.js | 2 +- services/users.js | 24 +++++++++++------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/routes/api/account/index.js b/routes/api/account/index.js index 44749a488..db11d391e 100644 --- a/routes/api/account/index.js +++ b/routes/api/account/index.js @@ -28,7 +28,7 @@ router.post('/email/confirm', (req, res, next) => { UsersService .verifyEmailConfirmation(token) - .then(([, referer]) => { + .then(({referer}) => { res.json({redirectUri: referer}); }) .catch((err) => { diff --git a/services/users.js b/services/users.js index d3751f0c8..e84e4f4d7 100644 --- a/services/users.js +++ b/services/users.js @@ -543,6 +543,9 @@ module.exports = class UsersService { return Promise.reject('email is required when creating a JWT for resetting passord'); } + // Conform the email to lowercase. + email = email.toLowerCase(); + const tokenOptions = { jwtid: uuid.v4(), algorithm: 'HS256', @@ -550,13 +553,12 @@ module.exports = class UsersService { subject: EMAIL_CONFIRM_JWT_SUBJECT }; - email = email.toLowerCase(); let userPromise; if (!userID) { - // if there is no userID, we're coming from the endpoint where a new user is re-requesting a confirmation email - // and we don't know the userID + // If there is no userID, we're coming from the endpoint where a new user + // is re-requesting a confirmation email and we don't know the userID. userPromise = UserModel.findOne({profiles: {$elemMatch: {id: email, provider: 'local'}}}); } else { userPromise = UsersService.findById(userID); @@ -564,7 +566,7 @@ module.exports = class UsersService { return userPromise.then((user) => { if (!user) { - return Promise.reject(new Error('user not found')); + return Promise.reject(errors.ErrNotFound); } // Get the profile representing the local account. @@ -575,13 +577,11 @@ module.exports = class UsersService { return Promise.reject(new Error('email address already confirmed')); } - const payload = { + return jwt.sign({ email, referer, userID: user.id - }; - - return jwt.sign(payload, process.env.TALK_SESSION_SECRET, tokenOptions); + }, process.env.TALK_SESSION_SECRET, tokenOptions); }); } @@ -598,8 +598,7 @@ module.exports = class UsersService { subject: EMAIL_CONFIRM_JWT_SUBJECT }) .then(({userID, email, referer}) => { - - const userUpdate = UserModel + return UserModel .update({ id: userID, profiles: { @@ -612,9 +611,8 @@ module.exports = class UsersService { $set: { 'profiles.$.metadata.confirmed_at': new Date() } - }); - - return Promise.all([userUpdate, referer]); + }) + .then(() => ({userID, email, referer})); }); } From 5d185e7aca7063e5d5a40bf0a993a6b56d7d066f Mon Sep 17 00:00:00 2001 From: David Jay Date: Tue, 31 Jan 2017 15:21:21 -0500 Subject: [PATCH 15/21] Updating port in globals. --- test/e2e/globals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/globals.js b/test/e2e/globals.js index 0f73e1d4a..da80a5dd9 100644 --- a/test/e2e/globals.js +++ b/test/e2e/globals.js @@ -1,6 +1,6 @@ module.exports = { waitForConditionTimeout: 8000, - baseUrl: 'http://localhost:3000', + baseUrl: 'http://localhost:3011', users: { admin: { email: 'admin@test.com', From e6e8ad5ca625e655fe9d809cafda7c44faf3f090 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 31 Jan 2017 13:29:23 -0700 Subject: [PATCH 16/21] close the resend email box after use. remove dead code --- client/coral-framework/actions/auth.js | 2 +- client/coral-framework/reducers/auth.js | 14 +++++++++++++- client/coral-sign-in/components/SignDialog.js | 7 ++++++- .../coral-sign-in/components/SignInContent.js | 8 +++++++- .../containers/SignInContainer.js | 18 +++++++++++------- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index bf7984579..592fe2be1 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -156,7 +156,7 @@ const confirmEmailFailure = () => ({type: actions.CONFIRM_EMAIL_FAILURE}); export const requestConfirmEmail = email => dispatch => { dispatch(confirmEmailRequest()); - coralApi('/users/resend-confirm', {method: 'POST', body: {email}}) + return coralApi('/users/resend-confirm', {method: 'POST', body: {email}}) .then(() => { dispatch(confirmEmailSuccess()); }) diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 54ffa37a6..6cb4e97cd 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -12,6 +12,8 @@ const initialState = Map({ passwordRequestSuccess: null, passwordRequestFailure: null, emailConfirmationFailure: false, + emailConfirmationLoading: false, + emailConfirmationSuccess: false, successSignUp: false }); @@ -35,6 +37,8 @@ export default function auth (state = initialState, action) { passwordRequestFailure: null, passwordRequestSuccess: null, emailConfirmationFailure: false, + emailConfirmationSuccess: false, + emailConfirmationLoading: false, successSignUp: false })); case actions.CHANGE_VIEW : @@ -104,7 +108,15 @@ export default function auth (state = initialState, action) { .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); + 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-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 d3d064c06..e27185b52 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -1,6 +1,6 @@ import React, {PropTypes} from 'react'; import Alert from './Alert'; -import {Button, FormField, Spinner} from 'coral-ui'; +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'; @@ -11,6 +11,8 @@ const SignInContent = ({ handleChangeEmail, emailToBeResent, handleResendConfirmation, + emailConfirmationLoading, + emailConfirmationSuccess, formData, ...props }) => { @@ -44,6 +46,8 @@ const SignInContent = ({ value={emailToBeResent} onChange={handleChangeEmail} /> + {emailConfirmationLoading && } + {emailConfirmationSuccess && } :
{ + setTimeout(() => { + + // allow success UI to be shown for a second, and then close the modal + this.props.handleClose(); + }, 2500); + }); } addError(name, error) { @@ -139,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 &&