Skip to content

Commit 013feb4

Browse files
committed
Merge branch 'feature/resend-verification-email' into dev
2 parents 0c2461f + 1458060 commit 013feb4

File tree

8 files changed

+248
-14
lines changed

8 files changed

+248
-14
lines changed

src/common/api/user.js

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ export default (apiEngine) => ({
44
verifyEmail: ({ token }) => apiEngine.post('/api/users/email/verify', {
55
data: { verifyEmailToken: token },
66
}),
7+
requestVerifyEmail: (form) => (
8+
apiEngine.post('/api/users/email/request-verify', { data: form })
9+
),
710
login: (user) => apiEngine.post('/api/users/login', { data: user }),
811
requestResetPassword: (form) => (
912
apiEngine.post('/api/users/password/request-reset', { data: form })
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import React, { Component, PropTypes } from 'react';
2+
import { connect } from 'react-redux';
3+
import { push } from 'react-router-redux';
4+
import { Field, reduxForm } from 'redux-form';
5+
import Alert from 'react-bootstrap/lib/Alert';
6+
import Button from 'react-bootstrap/lib/Button';
7+
// import validator from 'validator';
8+
import FormNames from '../../../constants/FormNames';
9+
import userAPI from '../../../api/user';
10+
import { validateForm } from '../../../actions/formActions';
11+
import { pushErrors } from '../../../actions/errorActions';
12+
import { Form, FormField, FormFooter } from '../../utils/BsForm';
13+
import configs from '../../../../../configs/project/client';
14+
15+
export let validate = (values) => {
16+
let errors = {};
17+
18+
// if (values.email && !validator.isEmail(values.email)) {
19+
// errors.email = 'Not an email';
20+
// }
21+
22+
if (!values.email) {
23+
errors.email = 'Required';
24+
}
25+
26+
if (configs.recaptcha && !values.recaptcha) {
27+
errors.recaptcha = 'Required';
28+
}
29+
30+
return errors;
31+
};
32+
33+
let asyncValidate = (values, dispatch) => {
34+
return dispatch(validateForm(
35+
FormNames.USER_VERIFY_EMAIL,
36+
'email',
37+
values.email
38+
)).then((json) => {
39+
let validationError = {};
40+
if (!json.isPassed) {
41+
validationError.email = json.message;
42+
throw validationError;
43+
}
44+
});
45+
};
46+
47+
class VerifyEmailForm extends Component {
48+
constructor() {
49+
super();
50+
this.handleSubmit = this._handleSubmit.bind(this);
51+
this.handleCancleClick = this._handleCancleClick.bind(this);
52+
}
53+
54+
componentDidMount() {
55+
let { email, initialize } = this.props;
56+
57+
if (email) {
58+
initialize({ email });
59+
}
60+
}
61+
62+
_handleSubmit(formData) {
63+
let { dispatch, apiEngine, initialize } = this.props;
64+
65+
return userAPI(apiEngine)
66+
.requestVerifyEmail(formData)
67+
.catch((err) => {
68+
dispatch(pushErrors(err));
69+
throw err;
70+
})
71+
.then((json) => {
72+
initialize({
73+
email: '',
74+
});
75+
});
76+
}
77+
78+
_handleCancleClick() {
79+
let { onCancel, dispatch } = this.props;
80+
81+
if (onCancel) {
82+
onCancel();
83+
} else {
84+
dispatch(push('/'));
85+
}
86+
}
87+
88+
render() {
89+
const {
90+
email,
91+
handleSubmit,
92+
submitSucceeded,
93+
submitFailed,
94+
error,
95+
pristine,
96+
submitting,
97+
invalid,
98+
} = this.props;
99+
100+
return (
101+
<Form horizontal onSubmit={handleSubmit(this.handleSubmit)}>
102+
{submitSucceeded && (
103+
<Alert bsStyle="success">A reset link is sent</Alert>
104+
)}
105+
{submitFailed && error && (<Alert bsStyle="danger">{error}</Alert>)}
106+
<Field
107+
label="Email"
108+
name="email"
109+
component={FormField}
110+
type="text"
111+
disabled={Boolean(email)}
112+
placeholder="Email"
113+
/>
114+
<Field
115+
label=" "
116+
name="recaptcha"
117+
component={FormField}
118+
type="recaptcha"
119+
/>
120+
<FormFooter>
121+
<Button
122+
type="submit"
123+
disabled={(!email && pristine) || submitting || invalid}
124+
>
125+
Send An Email to Verify My Email Address
126+
{submitting && (
127+
<i className="fa fa-spinner fa-spin" aria-hidden="true" />
128+
)}
129+
</Button>
130+
<Button
131+
bsStyle="link"
132+
onClick={this.handleCancleClick}
133+
>
134+
Cancel
135+
</Button>
136+
</FormFooter>
137+
</Form>
138+
);
139+
}
140+
};
141+
142+
VerifyEmailForm.propTypes = {
143+
email: PropTypes.string,
144+
onCancel: PropTypes.func,
145+
};
146+
147+
export default reduxForm({
148+
form: FormNames.USER_VERIFY_EMAIL,
149+
validate,
150+
asyncValidate,
151+
asyncBlurFields: ['email'],
152+
})(connect(state => ({
153+
apiEngine: state.apiEngine,
154+
}))(VerifyEmailForm));

src/common/components/pages/user/ShowPage.js

+43-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
22
import { connect } from 'react-redux';
33
import { Link } from 'react-router';
44
import PageHeader from 'react-bootstrap/lib/PageHeader';
5+
import Modal from 'react-bootstrap/lib/Modal';
56
import Row from 'react-bootstrap/lib/Row';
67
import Col from 'react-bootstrap/lib/Col';
78
import Button from 'react-bootstrap/lib/Button';
@@ -11,13 +12,17 @@ import Head from '../../widgets/Head';
1112
import PageLayout from '../../layouts/PageLayout';
1213
import Time from '../../widgets/Time';
1314
import RefreshImage from '../../utils/RefreshImage';
15+
import VerifyEmailForm from '../../forms/user/VerifyEmailForm';
1416

1517
class ShowPage extends Component {
1618
constructor(props) {
1719
super(props);
1820
this.state = {
1921
user: props.initialUser,
22+
isShowVerifyEmailModal: false,
2023
};
24+
this.openModal = this._openModal.bind(this);
25+
this.closeModal = this._closeModal.bind(this);
2126
}
2227

2328
componentDidMount() {
@@ -36,6 +41,35 @@ class ShowPage extends Component {
3641
});
3742
}
3843

44+
_openModal() {
45+
this.setState({ isShowVerifyEmailModal: true });
46+
}
47+
48+
_closeModal() {
49+
this.setState({ isShowVerifyEmailModal: false });
50+
}
51+
52+
renderModal() {
53+
let { isShowVerifyEmailModal, user } = this.state;
54+
55+
return (
56+
<Modal
57+
show={isShowVerifyEmailModal}
58+
onHide={this.closeModal}
59+
>
60+
<Modal.Header closeButton>
61+
<Modal.Title>Send Verification Mail</Modal.Title>
62+
</Modal.Header>
63+
<Modal.Body>
64+
<VerifyEmailForm
65+
email={user.email && user.email.value}
66+
onCancel={this.closeModal}
67+
/>
68+
</Modal.Body>
69+
</Modal>
70+
);
71+
}
72+
3973
render() {
4074
const { user } = this.state;
4175
return (
@@ -45,6 +79,7 @@ class ShowPage extends Component {
4579
'https://www.gstatic.com/firebasejs/live/3.0/firebase.js',
4680
]}
4781
/>
82+
{this.renderModal()}
4883
<Row>
4984
<Col md={12}>
5085
<Link to="/user/me/edit">
@@ -64,7 +99,14 @@ class ShowPage extends Component {
6499
<dt>name</dt>
65100
<dd>{user.name}</dd>
66101
<dt>email</dt>
67-
<dd>{user.email && user.email.value}</dd>
102+
<dd>
103+
{user.email && user.email.value}
104+
{user.email && !user.email.isVerified && (
105+
<Button onClick={this.openModal}>
106+
Verify Now
107+
</Button>
108+
)}
109+
</dd>
68110
<dt>updatedAt</dt>
69111
<dd>
70112
<Time value={user.updatedAt} format="YYYY-MM-DD" />

src/common/constants/FormNames.js

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export default {
33
USER_LOGIN: 'USER_LOGIN',
44
USER_EDIT: 'USER_EDIT',
55
USER_AVATAR: 'USER_AVATAR',
6+
USER_VERIFY_EMAIL: 'USER_VERIFY_EMAIL',
67
USER_CHANGE_PASSWORD: 'USER_CHANGE_PASSWORD',
78
USER_FORGET_PASSWORD: 'USER_FORGET_PASSWORD',
89
USER_RESET_PASSWORD: 'USER_RESET_PASSWORD',

src/server/controllers/formValidation.js

+19
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,25 @@ export default {
2222
},
2323
},
2424

25+
[FormNames.USER_VERIFY_EMAIL]: {
26+
email(req, res) {
27+
User.findOne({
28+
'email.value': req.body.value,
29+
}, handleDbError(res)((user) => {
30+
if (!user) {
31+
res.json({
32+
isPassed: false,
33+
message: 'This is an invalid account',
34+
});
35+
} else {
36+
res.json({
37+
isPassed: true,
38+
});
39+
}
40+
}));
41+
},
42+
},
43+
2544
[FormNames.USER_FORGET_PASSWORD]: {
2645
email(req, res) {
2746
User.findOne({

src/server/controllers/user.js

+9-9
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ export default {
4040
value: req.body.email,
4141
},
4242
password: req.body.password,
43+
nonce: {
44+
verifyEmail: Math.random(),
45+
},
4346
});
4447
user.save(handleDbError(res)((user) => {
4548
req.user = user;
@@ -55,15 +58,12 @@ export default {
5558
},
5659

5760
verifyEmail(req, res) {
58-
User.findById(req.decodedPayload._id, handleDbError(res)((user) => {
59-
if (user.email.isVerified) {
60-
return res.errors([Errors.TOKEN_REUSED]);
61-
}
62-
user.email.isVerified = true;
63-
user.email.verifiedAt = new Date();
64-
user.save(handleDbError(res)(() => {
65-
res.json({});
66-
}));
61+
let { user } = req;
62+
63+
user.email.isVerified = true;
64+
user.email.verifiedAt = new Date();
65+
user.save(handleDbError(res)(() => {
66+
res.json({});
6767
}));
6868
},
6969

src/server/models/User.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ let UserSchema = new mongoose.Schema({
5252
},
5353
},
5454
nonce: {
55-
password: Number,
55+
verifyEmail: Number,
56+
resetPassword: Number,
5657
},
5758
lastLoggedInAt: Date,
5859
}, {
@@ -73,6 +74,7 @@ UserSchema.methods.auth = function(password, cb) {
7374
UserSchema.methods.toVerifyEmailToken = function(cb) {
7475
const user = {
7576
_id: this._id,
77+
nonce: this.nonce.verifyEmail,
7678
};
7779
const token = jwt.sign(user, configs.jwt.verifyEmail.secret, {
7880
expiresIn: configs.jwt.verifyEmail.expiresIn,
@@ -83,7 +85,7 @@ UserSchema.methods.toVerifyEmailToken = function(cb) {
8385
UserSchema.methods.toResetPasswordToken = function(cb) {
8486
const user = {
8587
_id: this._id,
86-
nonce: this.nonce.password,
88+
nonce: this.nonce.resetPassword,
8789
};
8890
const token = jwt.sign(user, configs.jwt.resetPassword.secret, {
8991
expiresIn: configs.jwt.resetPassword.expiresIn,

src/server/routes/api.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,22 @@ export default ({ app }) => {
3131
'verifyEmailToken',
3232
configs.jwt.verifyEmail.secret
3333
),
34+
validate.verifyUserNonce('verifyEmail'),
3435
userController.verifyEmail
3536
);
37+
app.post('/api/users/email/request-verify',
38+
bodyParser.json,
39+
validate.form('user/VerifyEmailForm'),
40+
validate.recaptcha,
41+
userController.setNonce('verifyEmail'),
42+
mailController.sendVerification
43+
);
3644
app.post('/api/users/login', bodyParser.json, userController.login);
3745
app.post('/api/users/password/request-reset',
3846
bodyParser.json,
3947
validate.form('user/ForgetPasswordForm'),
4048
validate.recaptcha,
41-
userController.setNonce('password'),
49+
userController.setNonce('resetPassword'),
4250
mailController.sendResetPasswordLink
4351
);
4452
app.put('/api/users/password',
@@ -47,7 +55,7 @@ export default ({ app }) => {
4755
'resetPasswordToken',
4856
configs.jwt.resetPassword.secret
4957
),
50-
validate.verifyUserNonce('password'),
58+
validate.verifyUserNonce('resetPassword'),
5159
validate.form('user/ResetPasswordForm'),
5260
userController.resetPassword
5361
);
@@ -89,6 +97,11 @@ export default ({ app }) => {
8997
bodyParser.json,
9098
formValidationController[FormNames.USER_REGISTER].email
9199
);
100+
app.post(
101+
`/api/forms/${FormNames.USER_VERIFY_EMAIL}/fields/email/validation`,
102+
bodyParser.json,
103+
formValidationController[FormNames.USER_VERIFY_EMAIL].email
104+
);
92105
app.post(
93106
`/api/forms/${FormNames.USER_FORGET_PASSWORD}/fields/email/validation`,
94107
bodyParser.json,

0 commit comments

Comments
 (0)