Empty states, validation and removing availability

This commit is contained in:
Belen Curcio
2016-11-18 07:41:46 -03:00
13 changed files with 98 additions and 75 deletions
+10 -2
View File
@@ -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});
+2 -5
View File
@@ -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';
+1 -1
View File
@@ -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 {
+11 -7
View File
@@ -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;
}
@@ -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"
}
}
}
}
@@ -25,7 +25,7 @@ const SignInContent = ({handleChange, formData, ...props}) => (
{lang.t('signIn.or')}
</h1>
</div>
{ props.auth.signInError && <Alert>{props.auth.signInError}</Alert> }
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
<form onSubmit={props.handleSignIn}>
<FormField
id="email"
@@ -26,17 +26,16 @@ const SignUpContent = ({handleChange, formData, ...props}) => (
{lang.t('signIn.or')}
</h1>
</div>
{ props.auth.signUpError && <Alert>{props.auth.signUpError}</Alert> }
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
<form onSubmit={props.handleSignUp}>
<FormField
id="email"
type="email"
label={lang.t('signIn.email')}
value={formData.email}
showErrors={!props.auth.emailAvailable || props.showErrors}
errorMsg={!props.auth.emailAvailable ? lang.t('signIn.emailInUse') : props.errors.email}
showErrors={props.showErrors}
errorMsg={props.errors.email}
onChange={handleChange}
autoFocus
/>
<FormField
id="displayName"
@@ -1,12 +1,12 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import debounce from 'lodash.debounce';
import SignDialog from '../components/SignDialog';
import Button from 'coral-ui/components/Button';
import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
import {
changeView,
@@ -17,7 +17,8 @@ import {
fetchSignInFacebook,
fetchForgotPassword,
facebookCallback,
fetchCheckAvailability
invalidForm,
validForm
} from '../../coral-framework/actions/auth';
class SignInContainer extends Component {
@@ -44,6 +45,12 @@ class SignInContainer extends Component {
componentDidMount() {
window.authCallback = this.props.facebookCallback;
const {formData} = this.state;
const errors = Object.keys(formData).reduce((map, prop) => {
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(
+8 -2
View File
@@ -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'
}
}
};
+9 -6
View File
@@ -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);
});
});
+4 -2
View File
@@ -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",
+13 -13
View File
@@ -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;
+4 -6
View File
@@ -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');