diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 54970ba60..a1a44611f 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -1,3 +1,6 @@ +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from './../translations'; +const lang = new I18n(translations); import * as actions from '../constants/auth'; import {base, handleResp, getInit} from '../helpers/response'; @@ -27,7 +30,7 @@ export const fetchSignIn = (formData) => dispatch => { dispatch(hideSignInDialog()); dispatch(signInSuccess(user)); }) - .catch(() => dispatch(signInFailure('Email and/or password combination incorrect.'))); + .catch(() => dispatch(signInFailure(lang.t('error.emailPasswordError')))); }; // Sign In - Facebook @@ -75,7 +78,7 @@ export const fetchSignUp = formData => dispatch => { dispatch(changeView('SIGNIN')); }, 3000); }) - .catch((error) => dispatch(signUpFailure(error))); + .catch(() => dispatch(signUpFailure(lang.t('error.emailInUse')))); // We need to inprove error handling. TODO (bc) }; // Forgot Password Actions @@ -106,3 +109,8 @@ export const logout = () => dispatch => { .catch(error => dispatch(logOutFailure(error))); }; +// LogOut Actions + +export const validForm = () => ({type: actions.VALID_FORM}); +export const invalidForm = error => ({type: actions.INVALID_FORM, error}); + diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index 5c0135f58..a6a2c44f8 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -24,9 +24,6 @@ export const LOGOUT_REQUEST = 'LOGOUT_REQUEST'; export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; export const LOGOUT_FAILURE = 'LOGOUT_FAILURE'; -export const FETCH_AVAILABILITY_REQUEST = 'FETCH_AVAILABILITY_REQUEST'; -export const FETCH_AVAILABILITY_SUCCESS = 'FETCH_AVAILABILITY_SUCCESS'; -export const FETCH_AVAILABILITY_FAILURE = 'FETCH_AVAILABILITY_FAILURE'; +export const INVALID_FORM = 'INVALID_FORM'; +export const VALID_FORM = 'VALID_FORM'; -export const AVAILABLE_FIELD = 'AVAILABLE_FIELD'; -export const UNAVAILABLE_FIELD = 'UNAVAILABLE_FIELD'; diff --git a/client/coral-framework/helpers/error.js b/client/coral-framework/helpers/error.js index 2a48c056d..34ba253ba 100644 --- a/client/coral-framework/helpers/error.js +++ b/client/coral-framework/helpers/error.js @@ -1,5 +1,5 @@ import I18n from 'coral-framework/modules/i18n/i18n'; -import translations from './translations'; +import translations from './../translations'; const lang = new I18n(translations); export default { diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 6dc59a019..3e454892f 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -7,8 +7,7 @@ const initialState = Map({ user: null, showSignInDialog: false, view: 'SIGNIN', - signUpError: '', - emailAvailable: true, + error: '', successSignUp: false }); @@ -22,14 +21,12 @@ export default function auth (state = initialState, action) { isLoading: false, showSignInDialog: false, view: 'SIGNIN', - signInError: '', - signUpError: '', - emailAvailable: true, + error: '', successSignUp: false })); case actions.CHANGE_VIEW : return state - .set('signInError', '') + .set('error', '') .set('view', action.view); case actions.CLEAN_STATE: return initialState; @@ -43,7 +40,7 @@ export default function auth (state = initialState, action) { case actions.FETCH_SIGNIN_FAILURE: return state .set('isLoading', false) - .set('signInError', action.error); + .set('error', action.error); case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS: return state .set('user', action.user) @@ -57,6 +54,7 @@ export default function auth (state = initialState, action) { .set('isLoading', true); case actions.FETCH_SIGNUP_FAILURE: return state + .set('error', action.error) .set('isLoading', false); case actions.FETCH_SIGNUP_SUCCESS: return state @@ -66,6 +64,12 @@ export default function auth (state = initialState, action) { return state .set('loggedIn', false) .set('user', null); + case actions.INVALID_FORM: + return state + .set('error', action.error); + case actions.VALID_FORM: + return state + .set('error', ''); default : return state; } diff --git a/client/coral-framework/helpers/translations.json b/client/coral-framework/translations.json similarity index 59% rename from client/coral-framework/helpers/translations.json rename to client/coral-framework/translations.json index f8ba0d35f..4abd1ed03 100644 --- a/client/coral-framework/helpers/translations.json +++ b/client/coral-framework/translations.json @@ -4,7 +4,9 @@ "email": "Not a valid E-Mail", "password": "Password must be at least 8 characters", "displayName": "Display name is too short", - "confirmPassword": "Passwords don`t match. Please, check again" + "confirmPassword": "Passwords don`t match. Please, check again", + "emailPasswordError": "Email and/or password combination incorrect.", + "emailInUse": "Email address already in use" } }, "es": { @@ -12,7 +14,9 @@ "email": "No es un email válido", "password": "La contraseña debe tener por lo menos 8 caracteres", "displayName": "El nombre es muy corto", - "confirmPassword": "Las contraseñas no coinciden" + "confirmPassword": "Las contraseñas no coinciden", + "emailPasswordError": "Email y/o contraseña incorrecta.", + "emailInUse": "Email address already in use" } } -} +} \ No newline at end of file diff --git a/client/coral-sign-in/components/SignInContent.js b/client/coral-sign-in/components/SignInContent.js index 8aa688133..76a93f56c 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -25,7 +25,7 @@ const SignInContent = ({handleChange, formData, ...props}) => ( {lang.t('signIn.or')} - { props.auth.signInError && {props.auth.signInError} } + { props.auth.error && {props.auth.error} }
( {lang.t('signIn.or')} - { props.auth.signUpError && {props.auth.signUpError} } + { props.auth.error && {props.auth.error} } { + map[prop] = lang.t('signIn.requiredField'); + return map; + }, {}); + this.setState({errors}); } handleChange(e) { @@ -71,29 +78,23 @@ class SignInContainer extends Component { validation(name, value) { const {addError} = this; const {formData} = this.state; - const {checkAvailability} = this.props; if (!value.length) { - addError(name, 'Please, fill this field'); + addError(name, lang.t('signIn.requiredField')); } else if (name === 'confirmPassword' && formData.confirmPassword !== formData.password) { - addError('confirmPassword', 'Passwords don`t match. Please, check again'); + addError('confirmPassword', lang.t('signIn.passwordsDontMatch')); } else if (!validate[name](value)) { addError(name, errorMsj[name]); } else { const { [name]: prop, ...errors } = this.state.errors; // eslint-disable-line // Removes Error this.setState(state => ({...state, errors})); - // Checks Email Availability - if (name === 'email') { - debounce(() => checkAvailability({[name]: value}), 200, {'maxWait': 1000})(); - } } } isCompleted() { const {formData} = this.state; - const {emailAvailable} = this.props.auth; - return !Object.keys(formData).filter(prop => !formData[prop].length).length && emailAvailable; + return !Object.keys(formData).filter(prop => !formData[prop].length).length; } displayErrors(show = true) { @@ -103,9 +104,13 @@ class SignInContainer extends Component { handleSignUp(e) { e.preventDefault(); const {errors} = this.state; + const {fetchSignUp, validForm, invalidForm} = this.props; this.displayErrors(); if (this.isCompleted() && !Object.keys(errors).length) { - this.props.fetchSignUp(this.state.formData); + fetchSignUp(this.state.formData); + validForm(); + } else { + invalidForm(lang.t('signIn.checkTheForm')); } } @@ -118,10 +123,6 @@ class SignInContainer extends Component { this.props.hideSignInDialog(); } - changeView(view) { - this.props.changeView(view); - } - render() { const {auth, showSignInDialog} = this.props; return ( @@ -147,14 +148,15 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ facebookCallback: (err, data) => dispatch(facebookCallback(err, data)), - fetchSignUp: (formData) => dispatch(fetchSignUp(formData)), - fetchSignIn: (formData) => dispatch(fetchSignIn(formData)), + fetchSignUp: formData => dispatch(fetchSignUp(formData)), + fetchSignIn: formData => dispatch(fetchSignIn(formData)), fetchSignInFacebook: () => dispatch(fetchSignInFacebook()), - fetchForgotPassword: (formData) => dispatch(fetchForgotPassword(formData)), + fetchForgotPassword: formData => dispatch(fetchForgotPassword(formData)), showSignInDialog: () => dispatch(showSignInDialog()), - changeView: (view) => dispatch(changeView(view)), + changeView: view => dispatch(changeView(view)), handleClose: () => dispatch(hideSignInDialog()), - checkAvailability: (formData) => dispatch(fetchCheckAvailability(formData)) + invalidForm: error => dispatch(invalidForm(error)), + validForm: () => dispatch(validForm()) }); export default connect( diff --git a/client/coral-sign-in/translations.js b/client/coral-sign-in/translations.js index fdf7de0a0..4e1d35805 100644 --- a/client/coral-sign-in/translations.js +++ b/client/coral-sign-in/translations.js @@ -16,7 +16,10 @@ export default { displayName: 'Display Name', alreadyHaveAnAccount: 'Already have an account?', recoverPassword: 'Recover password', - emailInUse: 'Email address already in use' + emailInUse: 'Email address already in use', + requiredField: 'This field is required', + passwordsDontMatch: 'Passwords don\'t match.', + checkTheForm: 'Invalid Form. Please, check the fields' } }, es: { @@ -36,7 +39,10 @@ export default { displayName: 'Nombre', alreadyHaveAnAccount: 'Ya tienes una cuenta?', recoverPassword: 'Recuperar contraseña', - emailInUse: 'Este email se encuentra en uso' + emailInUse: 'Este email se encuentra en uso', + requiredField: 'Este campo es requerido', + passwordsDontMatch: 'Las contraseñas no coinciden', + checkTheForm: 'Formulario Inválido. Por favor, completa los campos' } } }; diff --git a/models/user.js b/models/user.js index a09415e96..83677e12c 100644 --- a/models/user.js +++ b/models/user.js @@ -13,11 +13,12 @@ const USER_ROLES = [ 'moderator' ]; -if (!process.env.TALK_SESSION_SECRET) { - throw new Error('\n////////////////////////////////////////////////////////////\n' + - '/// TALK_SESSION_SECRET must be defined to encode ///\n' + - '/// JSON Web Tokens and other auth functionality ///\n' + - '////////////////////////////////////////////////////////////'); +// In the event that the TALK_SESSION_SECRET is missing but we are testing, then +// set the process.env.TALK_SESSION_SECRET. +if (process.env.NODE_ENV === 'test' && !process.env.TALK_SESSION_SECRET) { + process.env.TALK_SESSION_SECRET = 'keyboard cat'; +} else if (!process.env.TALK_SESSION_SECRET) { + throw new Error('TALK_SESSION_SECRET must be defined to encode JSON Web Tokens and other auth functionality'); } // UserSchema is the mongoose schema defined as the representation of a User in @@ -311,9 +312,11 @@ UserService.createLocalUser = (email, password, displayName) => { user.save((err) => { if (err) { + if (err.code === 11000) { + return reject('Email address already in use'); + } return reject(err); } - return resolve(user); }); }); diff --git a/package.json b/package.json index 822480c5a..dd9f8db8a 100644 --- a/package.json +++ b/package.json @@ -45,13 +45,15 @@ "express-session": "^1.14.2", "helmet": "^3.1.0", "lodash.debounce": "^4.0.8", - "jsonwebtoken": "^7.1.9", - "lodash": "^4.16.6", "mongoose": "^4.6.5", "morgan": "^1.7.0", "passport": "^0.3.2", "passport-facebook": "^2.1.1", "passport-local": "^1.0.0", + "jsonwebtoken": "^7.1.9", + "lodash": "^4.16.6", + "mongoose": "^4.6.5", + "morgan": "^1.7.0", "nodemailer": "^2.6.4", "prompt": "^1.0.0", "redis": "^2.6.3", diff --git a/routes/api/user/index.js b/routes/api/user/index.js index bbe66c647..39bb8a5c9 100644 --- a/routes/api/user/index.js +++ b/routes/api/user/index.js @@ -58,6 +58,19 @@ router.post('/:user_id/role', (req, res, next) => { .catch(next); }); +router.post('/', (req, res, next) => { + const {email, password, displayName} = req.body; + + User + .createLocalUser(email, password, displayName) + .then(user => { + res.status(201).send(user); + }) + .catch(err => { + next(err); + }); +}); + /** * expects 2 fields in the body of the request * 1) the token that was in the url of the email link {String} @@ -121,17 +134,4 @@ router.post('/request-password-reset', (req, res, next) => { }); }); -router.post('/', (req, res, next) => { - const {email, password, displayName} = req.body; - - User - .createLocalUser(email, password, displayName) - .then(user => { - res.status(201).send(user); - }) - .catch(err => { - next(err); - }); -}); - module.exports = router; diff --git a/services/mailer.js b/services/mailer.js index cdb23f370..4da8024ca 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -1,22 +1,20 @@ const nodemailer = require('nodemailer'); if (!process.env.TALK_SMTP_CONNECTION_URL) { - throw new Error('\n///////////////////////////////////////////////////////////////\n' + - '/// TALK_SMTP_CONNECTION_URL should be defined if you would ///\n' + - '/// like to send password reset emails from Talk ///\n' + - '///////////////////////////////////////////////////////////////'); + console.error('TALK_SMTP_CONNECTION_URL should be defined if you would like to send password reset emails from Talk'); } -const transporter = nodemailer.createTransport(process.env.TALK_SMTP_CONNECTION_URL); +const defaultTransporter = nodemailer.createTransport(process.env.TALK_SMTP_CONNECTION_URL); const mailer = { + /** * sendSimple * * @param {Object} {from, to, subject, text = '', html = ''} * @returns */ - sendSimple ({from, to, subject, text = '', html = ''}) { + sendSimple({from, to, subject, text = '', html = '', transporter = defaultTransporter}) { return new Promise((resolve, reject) => { if (!from) { reject('sendSimple requires a from address');