Skip to content

Commit

Permalink
Merge branch 'feature/resend-verification-email' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
gocreating committed Oct 31, 2016
2 parents 0c2461f + 1458060 commit 013feb4
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 14 deletions.
3 changes: 3 additions & 0 deletions src/common/api/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export default (apiEngine) => ({
verifyEmail: ({ token }) => apiEngine.post('/api/users/email/verify', {
data: { verifyEmailToken: token },
}),
requestVerifyEmail: (form) => (
apiEngine.post('/api/users/email/request-verify', { data: form })
),
login: (user) => apiEngine.post('/api/users/login', { data: user }),
requestResetPassword: (form) => (
apiEngine.post('/api/users/password/request-reset', { data: form })
Expand Down
154 changes: 154 additions & 0 deletions src/common/components/forms/user/VerifyEmailForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { push } from 'react-router-redux';
import { Field, reduxForm } from 'redux-form';
import Alert from 'react-bootstrap/lib/Alert';
import Button from 'react-bootstrap/lib/Button';
// import validator from 'validator';
import FormNames from '../../../constants/FormNames';
import userAPI from '../../../api/user';
import { validateForm } from '../../../actions/formActions';
import { pushErrors } from '../../../actions/errorActions';
import { Form, FormField, FormFooter } from '../../utils/BsForm';
import configs from '../../../../../configs/project/client';

export let validate = (values) => {
let errors = {};

// if (values.email && !validator.isEmail(values.email)) {
// errors.email = 'Not an email';
// }

if (!values.email) {
errors.email = 'Required';
}

if (configs.recaptcha && !values.recaptcha) {
errors.recaptcha = 'Required';
}

return errors;
};

let asyncValidate = (values, dispatch) => {
return dispatch(validateForm(
FormNames.USER_VERIFY_EMAIL,
'email',
values.email
)).then((json) => {
let validationError = {};
if (!json.isPassed) {
validationError.email = json.message;
throw validationError;
}
});
};

class VerifyEmailForm extends Component {
constructor() {
super();
this.handleSubmit = this._handleSubmit.bind(this);
this.handleCancleClick = this._handleCancleClick.bind(this);
}

componentDidMount() {
let { email, initialize } = this.props;

if (email) {
initialize({ email });
}
}

_handleSubmit(formData) {
let { dispatch, apiEngine, initialize } = this.props;

return userAPI(apiEngine)
.requestVerifyEmail(formData)
.catch((err) => {
dispatch(pushErrors(err));
throw err;
})
.then((json) => {
initialize({
email: '',
});
});
}

_handleCancleClick() {
let { onCancel, dispatch } = this.props;

if (onCancel) {
onCancel();
} else {
dispatch(push('/'));
}
}

render() {
const {
email,
handleSubmit,
submitSucceeded,
submitFailed,
error,
pristine,
submitting,
invalid,
} = this.props;

return (
<Form horizontal onSubmit={handleSubmit(this.handleSubmit)}>
{submitSucceeded && (
<Alert bsStyle="success">A reset link is sent</Alert>
)}
{submitFailed && error && (<Alert bsStyle="danger">{error}</Alert>)}
<Field
label="Email"
name="email"
component={FormField}
type="text"
disabled={Boolean(email)}
placeholder="Email"
/>
<Field
label=" "
name="recaptcha"
component={FormField}
type="recaptcha"
/>
<FormFooter>
<Button
type="submit"
disabled={(!email && pristine) || submitting || invalid}
>
Send An Email to Verify My Email Address
{submitting && (
<i className="fa fa-spinner fa-spin" aria-hidden="true" />
)}
</Button>
<Button
bsStyle="link"
onClick={this.handleCancleClick}
>
Cancel
</Button>
</FormFooter>
</Form>
);
}
};

VerifyEmailForm.propTypes = {
email: PropTypes.string,
onCancel: PropTypes.func,
};

export default reduxForm({
form: FormNames.USER_VERIFY_EMAIL,
validate,
asyncValidate,
asyncBlurFields: ['email'],
})(connect(state => ({
apiEngine: state.apiEngine,
}))(VerifyEmailForm));
44 changes: 43 additions & 1 deletion src/common/components/pages/user/ShowPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import PageHeader from 'react-bootstrap/lib/PageHeader';
import Modal from 'react-bootstrap/lib/Modal';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
import Button from 'react-bootstrap/lib/Button';
Expand All @@ -11,13 +12,17 @@ import Head from '../../widgets/Head';
import PageLayout from '../../layouts/PageLayout';
import Time from '../../widgets/Time';
import RefreshImage from '../../utils/RefreshImage';
import VerifyEmailForm from '../../forms/user/VerifyEmailForm';

class ShowPage extends Component {
constructor(props) {
super(props);
this.state = {
user: props.initialUser,
isShowVerifyEmailModal: false,
};
this.openModal = this._openModal.bind(this);
this.closeModal = this._closeModal.bind(this);
}

componentDidMount() {
Expand All @@ -36,6 +41,35 @@ class ShowPage extends Component {
});
}

_openModal() {
this.setState({ isShowVerifyEmailModal: true });
}

_closeModal() {
this.setState({ isShowVerifyEmailModal: false });
}

renderModal() {
let { isShowVerifyEmailModal, user } = this.state;

return (
<Modal
show={isShowVerifyEmailModal}
onHide={this.closeModal}
>
<Modal.Header closeButton>
<Modal.Title>Send Verification Mail</Modal.Title>
</Modal.Header>
<Modal.Body>
<VerifyEmailForm
email={user.email && user.email.value}
onCancel={this.closeModal}
/>
</Modal.Body>
</Modal>
);
}

render() {
const { user } = this.state;
return (
Expand All @@ -45,6 +79,7 @@ class ShowPage extends Component {
'https://www.gstatic.com/firebasejs/live/3.0/firebase.js',
]}
/>
{this.renderModal()}
<Row>
<Col md={12}>
<Link to="/user/me/edit">
Expand All @@ -64,7 +99,14 @@ class ShowPage extends Component {
<dt>name</dt>
<dd>{user.name}</dd>
<dt>email</dt>
<dd>{user.email && user.email.value}</dd>
<dd>
{user.email && user.email.value}
{user.email && !user.email.isVerified && (
<Button onClick={this.openModal}>
Verify Now
</Button>
)}
</dd>
<dt>updatedAt</dt>
<dd>
<Time value={user.updatedAt} format="YYYY-MM-DD" />
Expand Down
1 change: 1 addition & 0 deletions src/common/constants/FormNames.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export default {
USER_LOGIN: 'USER_LOGIN',
USER_EDIT: 'USER_EDIT',
USER_AVATAR: 'USER_AVATAR',
USER_VERIFY_EMAIL: 'USER_VERIFY_EMAIL',
USER_CHANGE_PASSWORD: 'USER_CHANGE_PASSWORD',
USER_FORGET_PASSWORD: 'USER_FORGET_PASSWORD',
USER_RESET_PASSWORD: 'USER_RESET_PASSWORD',
Expand Down
19 changes: 19 additions & 0 deletions src/server/controllers/formValidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,25 @@ export default {
},
},

[FormNames.USER_VERIFY_EMAIL]: {
email(req, res) {
User.findOne({
'email.value': req.body.value,
}, handleDbError(res)((user) => {
if (!user) {
res.json({
isPassed: false,
message: 'This is an invalid account',
});
} else {
res.json({
isPassed: true,
});
}
}));
},
},

[FormNames.USER_FORGET_PASSWORD]: {
email(req, res) {
User.findOne({
Expand Down
18 changes: 9 additions & 9 deletions src/server/controllers/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export default {
value: req.body.email,
},
password: req.body.password,
nonce: {
verifyEmail: Math.random(),
},
});
user.save(handleDbError(res)((user) => {
req.user = user;
Expand All @@ -55,15 +58,12 @@ export default {
},

verifyEmail(req, res) {
User.findById(req.decodedPayload._id, handleDbError(res)((user) => {
if (user.email.isVerified) {
return res.errors([Errors.TOKEN_REUSED]);
}
user.email.isVerified = true;
user.email.verifiedAt = new Date();
user.save(handleDbError(res)(() => {
res.json({});
}));
let { user } = req;

user.email.isVerified = true;
user.email.verifiedAt = new Date();
user.save(handleDbError(res)(() => {
res.json({});
}));
},

Expand Down
6 changes: 4 additions & 2 deletions src/server/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ let UserSchema = new mongoose.Schema({
},
},
nonce: {
password: Number,
verifyEmail: Number,
resetPassword: Number,
},
lastLoggedInAt: Date,
}, {
Expand All @@ -73,6 +74,7 @@ UserSchema.methods.auth = function(password, cb) {
UserSchema.methods.toVerifyEmailToken = function(cb) {
const user = {
_id: this._id,
nonce: this.nonce.verifyEmail,
};
const token = jwt.sign(user, configs.jwt.verifyEmail.secret, {
expiresIn: configs.jwt.verifyEmail.expiresIn,
Expand All @@ -83,7 +85,7 @@ UserSchema.methods.toVerifyEmailToken = function(cb) {
UserSchema.methods.toResetPasswordToken = function(cb) {
const user = {
_id: this._id,
nonce: this.nonce.password,
nonce: this.nonce.resetPassword,
};
const token = jwt.sign(user, configs.jwt.resetPassword.secret, {
expiresIn: configs.jwt.resetPassword.expiresIn,
Expand Down
17 changes: 15 additions & 2 deletions src/server/routes/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,22 @@ export default ({ app }) => {
'verifyEmailToken',
configs.jwt.verifyEmail.secret
),
validate.verifyUserNonce('verifyEmail'),
userController.verifyEmail
);
app.post('/api/users/email/request-verify',
bodyParser.json,
validate.form('user/VerifyEmailForm'),
validate.recaptcha,
userController.setNonce('verifyEmail'),
mailController.sendVerification
);
app.post('/api/users/login', bodyParser.json, userController.login);
app.post('/api/users/password/request-reset',
bodyParser.json,
validate.form('user/ForgetPasswordForm'),
validate.recaptcha,
userController.setNonce('password'),
userController.setNonce('resetPassword'),
mailController.sendResetPasswordLink
);
app.put('/api/users/password',
Expand All @@ -47,7 +55,7 @@ export default ({ app }) => {
'resetPasswordToken',
configs.jwt.resetPassword.secret
),
validate.verifyUserNonce('password'),
validate.verifyUserNonce('resetPassword'),
validate.form('user/ResetPasswordForm'),
userController.resetPassword
);
Expand Down Expand Up @@ -89,6 +97,11 @@ export default ({ app }) => {
bodyParser.json,
formValidationController[FormNames.USER_REGISTER].email
);
app.post(
`/api/forms/${FormNames.USER_VERIFY_EMAIL}/fields/email/validation`,
bodyParser.json,
formValidationController[FormNames.USER_VERIFY_EMAIL].email
);
app.post(
`/api/forms/${FormNames.USER_FORGET_PASSWORD}/fields/email/validation`,
bodyParser.json,
Expand Down

0 comments on commit 013feb4

Please sign in to comment.