resend verify email

This commit is contained in:
Riley Davis
2017-01-31 12:04:19 -07:00
parent 976c821715
commit 56ddc49386
13 changed files with 233 additions and 92 deletions
+28 -3
View File
@@ -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());
});
};
+5
View File
@@ -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';
+10 -4
View File
@@ -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();
+4
View File
@@ -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;
}
+2
View File
@@ -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 _",
@@ -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}) => (
<div>
<div className={styles.header}>
<h1>
{lang.t('signIn.signIn')}
</h1>
</div>
<div className={styles.socialConnections}>
<Button cStyle="facebook" onClick={props.fetchSignInFacebook} full>
{lang.t('signIn.facebookSignIn')}
</Button>
</div>
<div className={styles.separator}>
<h1>
{lang.t('signIn.or')}
</h1>
</div>
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
<form onSubmit={props.handleSignIn}>
<FormField
id="email"
type="email"
label={lang.t('signIn.email')}
value={formData.email}
onChange={handleChange}
/>
<FormField
id="password"
type="password"
label={lang.t('signIn.password')}
value={formData.password}
onChange={handleChange}
/>
<div className={styles.action}>
{
!props.auth.isLoading ?
<Button id='coralLogInButton' type="submit" cStyle="black" className={styles.signInButton} full>
{lang.t('signIn.signIn')}
</Button>
:
<Spinner />
}
const SignInContent = ({
handleChange,
handleChangeEmail,
emailToBeResent,
handleResendConfirmation,
formData,
...props
}) => {
return (
<div>
<div className={styles.header}>
<h1>
{props.auth.emailConfirmationFailure ? lang.t('signIn.emailConfirmCTA') : lang.t('signIn.signIn')}
</h1>
</div>
<div className={styles.socialConnections}>
<Button cStyle="facebook" onClick={props.fetchSignInFacebook} full>
{lang.t('signIn.facebookSignIn')}
</Button>
</div>
<div className={styles.separator}>
<h1>
{lang.t('signIn.or')}
</h1>
</div>
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
{
props.auth.emailConfirmationFailure
? <form onSubmit={handleResendConfirmation}>
<p>{lang.t('signIn.requestNewConfirmEmail')}</p>
<FormField
id="confirm-email"
type="email"
label={lang.t('signIn.email')}
value={emailToBeResent}
onChange={handleChangeEmail} />
<Button id='resendConfirmEmail' type='submit' cStyle='black' full>Send Email</Button>
</form>
: <form onSubmit={props.handleSignIn}>
<FormField
id="email"
type="email"
label={lang.t('signIn.email')}
value={formData.email}
onChange={handleChange}
/>
<FormField
id="password"
type="password"
label={lang.t('signIn.password')}
value={formData.password}
onChange={handleChange}
/>
<div className={styles.action}>
{
!props.auth.isLoading ?
<Button id='coralLogInButton' type="submit" cStyle="black" className={styles.signInButton} full>
{lang.t('signIn.signIn')}
</Button>
:
<Spinner />
}
</div>
</form>
}
<div className={styles.footer}>
<span><a onClick={() => props.changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')}</a></span>
<span>
{lang.t('signIn.needAnAccount')}
<a onClick={() => props.changeView('SIGNUP')} id='coralRegister'>
{lang.t('signIn.register')}
</a>
</span>
</div>
</form>
<div className={styles.footer}>
<span><a onClick={() => props.changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')}</a></span>
<span>
{lang.t('signIn.needAnAccount')}
<a onClick={() => props.changeView('SIGNUP')} id='coralRegister'>
{lang.t('signIn.register')}
</a>
</span>
</div>
</div>
);
);
};
SignInContent.propTypes = {
handleResendConfirmation: PropTypes.func.isRequired,
handleChangeEmail: PropTypes.func.isRequired,
emailToBeResent: PropTypes.string.isRequired
};
export default SignInContent;
@@ -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()),
+4 -4
View File
@@ -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',
+20
View File
@@ -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
};
+20
View File
@@ -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
+3 -2
View File
@@ -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));
}
}
+35 -25
View File
@@ -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);
});
}
/**
+3
View File
@@ -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);
})