From 5e521b832c69b9126d5e9489725f1b02589dc708 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Fri, 27 Jan 2017 15:04:42 -0700 Subject: [PATCH 01/10] 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 976c821715e58885d581e0ae5ab901981a49784a Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Mon, 30 Jan 2017 16:00:24 -0700 Subject: [PATCH 02/10] 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 03/10] 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 56ddc493869b850daffa88c24f886cbb4d606724 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 31 Jan 2017 12:04:19 -0700 Subject: [PATCH 04/10] 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 05/10] 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 06/10] 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 a13b1c95d65ee15e8e9e4f8b9dd795248b3efc8d Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 31 Jan 2017 12:57:09 -0700 Subject: [PATCH 07/10] 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 08/10] 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 09/10] 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 &&