From bc0a220ffc07b0cea5be510f32852d203d68e250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sat, 29 Oct 2016 16:53:11 +0800 Subject: [PATCH 01/17] Rename form names --- src/common/components/forms/user/LoginForm.js | 2 +- src/common/components/forms/user/RegisterForm.js | 4 ++-- src/server/controllers/formValidation.js | 2 +- src/server/routes/api.js | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/common/components/forms/user/LoginForm.js b/src/common/components/forms/user/LoginForm.js index 3a0358b..c17e489 100644 --- a/src/common/components/forms/user/LoginForm.js +++ b/src/common/components/forms/user/LoginForm.js @@ -107,7 +107,7 @@ class LoginForm extends Component { }; export default reduxForm({ - form: 'login', + form: 'userLogin', validate, })(connect(state => ({ apiEngine: state.apiEngine, diff --git a/src/common/components/forms/user/RegisterForm.js b/src/common/components/forms/user/RegisterForm.js index 6929a9c..b012cdc 100644 --- a/src/common/components/forms/user/RegisterForm.js +++ b/src/common/components/forms/user/RegisterForm.js @@ -34,7 +34,7 @@ const validate = (values) => { }; let asyncValidate = (values, dispatch) => { - return dispatch(validateForm('register', 'email', values.email)) + return dispatch(validateForm('userRegister', 'email', values.email)) .then((json) => { let validationError = {}; if (!json.isPassed) { @@ -119,7 +119,7 @@ class RegisterForm extends Component { }; export default reduxForm({ - form: 'register', + form: 'userRegister', validate, asyncValidate, asyncBlurFields: ['email'], diff --git a/src/server/controllers/formValidation.js b/src/server/controllers/formValidation.js index b4f1631..8034ecf 100644 --- a/src/server/controllers/formValidation.js +++ b/src/server/controllers/formValidation.js @@ -2,7 +2,7 @@ import { handleDbError } from '../decorators/handleError'; import User from '../models/User'; export default { - register: { + userRegister: { email(req, res) { User.findOne({ 'email.value': req.body.value, diff --git a/src/server/routes/api.js b/src/server/routes/api.js index 3824ba7..e013b13 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -63,9 +63,9 @@ export default ({ app }) => { userController.uploadAvatar); // form - app.post('/api/forms/register/fields/email/validation', + app.post('/api/forms/userRegister/fields/email/validation', bodyParser.json, - formValidationController.register.email + formValidationController.userRegister.email ); // locale From 14bf8a2185f9a8dfbfe2e06f1d31224278ad048a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sat, 29 Oct 2016 20:11:50 +0800 Subject: [PATCH 02/17] Add middleware bodyParser.jwt --- src/server/middlewares/bodyParser.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/server/middlewares/bodyParser.js b/src/server/middlewares/bodyParser.js index 466df05..0d3d5e7 100644 --- a/src/server/middlewares/bodyParser.js +++ b/src/server/middlewares/bodyParser.js @@ -1,8 +1,18 @@ import bodyParser from 'body-parser'; +import jwt from 'jsonwebtoken'; +import { handleJwtError } from '../decorators/handleError'; export default { // parse application/x-www-form-urlencoded urlencoded: bodyParser.urlencoded({ extended: false }), // parse application/json json: bodyParser.json(), + jwt: (key, secret) => (req, res, next) => { + let token = req.body[key]; + + jwt.verify(token, secret, handleJwtError(res)((decoded) => { + req.decodedPayload = decoded; + next(); + })); + }, }; From fbe852d3397175d5bc500da480477074ad32cb52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sun, 30 Oct 2016 01:18:43 +0800 Subject: [PATCH 03/17] Add util function tokenToURL --- src/server/utils/tokenToURL.js | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/server/utils/tokenToURL.js diff --git a/src/server/utils/tokenToURL.js b/src/server/utils/tokenToURL.js new file mode 100644 index 0000000..c547d52 --- /dev/null +++ b/src/server/utils/tokenToURL.js @@ -0,0 +1,5 @@ +import configs from '../../../configs/project/server'; + +export default (baseURL, token) => ( + `${configs.host[process.env.NODE_ENV]}${baseURL}?token=${token}` +); From eaace11e9281f75cd70777046bba36f0ff9ea13a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sat, 29 Oct 2016 17:38:31 +0800 Subject: [PATCH 04/17] Refactor email verification --- configs/project/server.js | 2 +- src/common/api/user.js | 4 +-- ...VerificationPage.js => VerifyEmailPage.js} | 2 +- src/common/routes/user/index.js | 2 +- .../user/{verification.js => verifyEmail.js} | 4 +-- src/server/components/VerificationMail.js | 26 ---------------- src/server/components/VerifyEmailMail.js | 25 ++++++++++++++++ src/server/controllers/mail.js | 6 ++-- src/server/controllers/user.js | 30 +++++++------------ src/server/models/User.js | 8 ++--- src/server/routes/api.js | 8 +++-- 11 files changed, 56 insertions(+), 61 deletions(-) rename src/common/components/pages/user/{VerificationPage.js => VerifyEmailPage.js} (96%) rename src/common/routes/user/{verification.js => verifyEmail.js} (52%) delete mode 100644 src/server/components/VerificationMail.js create mode 100644 src/server/components/VerifyEmailMail.js diff --git a/configs/project/server.js b/configs/project/server.js index 7fb20f9..cfba7ea 100755 --- a/configs/project/server.js +++ b/configs/project/server.js @@ -18,7 +18,7 @@ if (process.env.TRAVIS) { secret: '4eO5viHe23', expiresIn: 60 * 60 * 24 * 3, // in seconds }, - verification: { + verifyEmail: { secret: 'df5s6sdHdjJdRg56', expiresIn: 60 * 60, // in seconds }, diff --git a/src/common/api/user.js b/src/common/api/user.js index 0d2b7bd..ef1bbab 100644 --- a/src/common/api/user.js +++ b/src/common/api/user.js @@ -1,8 +1,8 @@ export default (apiEngine) => ({ list: ({ page }) => apiEngine.get('/api/users', { params: { page } }), register: (user) => apiEngine.post('/api/users', { data: user }), - verify: ({ token }) => apiEngine.post('/api/users/verification', { - data: { verificationToken: token }, + verifyEmail: ({ token }) => apiEngine.post('/api/users/email/verify', { + data: { verifyEmailToken: token }, }), login: (user) => apiEngine.post('/api/users/login', { data: user }), logout: () => apiEngine.get('/api/users/logout'), diff --git a/src/common/components/pages/user/VerificationPage.js b/src/common/components/pages/user/VerifyEmailPage.js similarity index 96% rename from src/common/components/pages/user/VerificationPage.js rename to src/common/components/pages/user/VerifyEmailPage.js index e32e08b..db99968 100644 --- a/src/common/components/pages/user/VerificationPage.js +++ b/src/common/components/pages/user/VerifyEmailPage.js @@ -19,7 +19,7 @@ class VerificationPage extends React.Component { let { dispatch, apiEngine, location } = this.props; if (process.env.BROWSER) { userAPI(apiEngine) - .verify({ token: location.query.token }) + .verifyEmail({ token: location.query.token }) .catch((err) => { this.setState({ isVerifying: false, diff --git a/src/common/routes/user/index.js b/src/common/routes/user/index.js index 84141ad..a64bf80 100644 --- a/src/common/routes/user/index.js +++ b/src/common/routes/user/index.js @@ -4,7 +4,7 @@ export default (store) => ({ require.ensure([], (require) => { cb(null, [ require('./register').default(store), - require('./verification').default(store), + require('./verifyEmail').default(store), require('./login').default(store), require('./edit').default(store), require('./logout').default(store), diff --git a/src/common/routes/user/verification.js b/src/common/routes/user/verifyEmail.js similarity index 52% rename from src/common/routes/user/verification.js rename to src/common/routes/user/verifyEmail.js index dcad9bb..4858faf 100644 --- a/src/common/routes/user/verification.js +++ b/src/common/routes/user/verifyEmail.js @@ -1,8 +1,8 @@ export default (store) => ({ - path: 'verification', + path: 'email/verify', getComponent(nextState, cb) { require.ensure([], (require) => { - cb(null, require('../../components/pages/user/VerificationPage').default); + cb(null, require('../../components/pages/user/VerifyEmailPage').default); }); }, }); diff --git a/src/server/components/VerificationMail.js b/src/server/components/VerificationMail.js deleted file mode 100644 index 027b31a..0000000 --- a/src/server/components/VerificationMail.js +++ /dev/null @@ -1,26 +0,0 @@ -import React, { PropTypes } from 'react'; -import configs from '../../../configs/project/server'; - -let tokenToURL = (token) => ( - `${configs.host[process.env.NODE_ENV]}` + - `/user/verification?token=${token}` -); - -let VerfificationMail = ({ token }) => ( -
-

- Please click the following link to verify your account. -

-

- - {tokenToURL(token)} - -

-
-); - -VerfificationMail.propTypes = { - token: PropTypes.string, -}; - -export default VerfificationMail; diff --git a/src/server/components/VerifyEmailMail.js b/src/server/components/VerifyEmailMail.js new file mode 100644 index 0000000..1633397 --- /dev/null +++ b/src/server/components/VerifyEmailMail.js @@ -0,0 +1,25 @@ +import React, { PropTypes } from 'react'; +import tokenToURL from '../utils/tokenToURL'; + +let VerifyEmailMail = ({ token }) => { + let url = tokenToURL('/user/email/verify', token); + + return ( +
+

+ Please click the following link to verify your account. +

+

+ + {url} + +

+
+ ); +}; + +VerifyEmailMail.propTypes = { + token: PropTypes.string, +}; + +export default VerifyEmailMail; diff --git a/src/server/controllers/mail.js b/src/server/controllers/mail.js index 847a429..ca7bbf7 100644 --- a/src/server/controllers/mail.js +++ b/src/server/controllers/mail.js @@ -2,12 +2,12 @@ import React from 'react'; import { renderToString } from 'react-dom/server'; import Errors from '../../common/constants/Errors'; import nodemailerAPI from '../api/nodemailer'; -import VerfificationMail from '../components/VerificationMail'; +import VerifyEmailMail from '../components/VerifyEmailMail'; export default { sendVerification(req, res) { let { user } = req; - let token = user.toVerificationToken(); + let token = user.toVerifyEmailToken(); nodemailerAPI() .sendMail({ @@ -18,7 +18,7 @@ export default { ), subject: 'Email Verification', html: renderToString( - + ), }) .catch((err) => { diff --git a/src/server/controllers/user.js b/src/server/controllers/user.js index ada9c39..7733563 100644 --- a/src/server/controllers/user.js +++ b/src/server/controllers/user.js @@ -52,25 +52,17 @@ export default { })); }, - verify(req, res) { - let token = req.body.verificationToken; - - jwt.verify( - token, - configs.jwt.verification.secret, - handleJwtError(res)(({ _id }) => { - User.findById(_id, handleDbError(res)((user) => { - if (user.email.isVerified) { - return res.errors([Errors.TOKEN_REUSED]); - } - user.email.isVerified = true; - user.verifiedAt = new Date(); - user.save(handleDbError(res)(() => { - res.json({}); - })); - })); - }) - ); + 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({}); + })); + })); }, login(req, res) { diff --git a/src/server/models/User.js b/src/server/models/User.js index 3096716..4a250c6 100644 --- a/src/server/models/User.js +++ b/src/server/models/User.js @@ -28,6 +28,7 @@ let UserSchema = new mongoose.Schema({ type: Boolean, default: false, }, + verifiedAt: Date, }, password: { type: String, @@ -47,7 +48,6 @@ let UserSchema = new mongoose.Schema({ linkedin: Object, }, }, - verifiedAt: Date, lastLoggedInAt: Date, }, { versionKey: false, @@ -64,12 +64,12 @@ UserSchema.methods.auth = function(password, cb) { cb(null, isAuthenticated); }; -UserSchema.methods.toVerificationToken = function(cb) { +UserSchema.methods.toVerifyEmailToken = function(cb) { const user = { _id: this._id, }; - const token = jwt.sign(user, configs.jwt.verification.secret, { - expiresIn: configs.jwt.verification.expiresIn, + const token = jwt.sign(user, configs.jwt.verifyEmail.secret, { + expiresIn: configs.jwt.verifyEmail.expiresIn, }); return token; }; diff --git a/src/server/routes/api.js b/src/server/routes/api.js index e013b13..a7a75cf 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -25,9 +25,13 @@ export default ({ app }) => { userController.create, mailController.sendVerification ); - app.post('/api/users/verification', + app.post('/api/users/email/verify', bodyParser.json, - userController.verify + bodyParser.jwt( + 'verifyEmailToken', + configs.jwt.verifyEmail.secret + ), + userController.verifyEmail ); app.post('/api/users/login', bodyParser.json, userController.login); app.get('/api/users/logout', userController.logout); From 8a91296f09a7184edc76f179920c0f9a68c1a38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sat, 29 Oct 2016 17:15:13 +0800 Subject: [PATCH 05/17] Add server side form validation API for User#ForgetPassword --- src/server/controllers/formValidation.js | 19 +++++++++++++++++++ src/server/routes/api.js | 4 ++++ 2 files changed, 23 insertions(+) diff --git a/src/server/controllers/formValidation.js b/src/server/controllers/formValidation.js index 8034ecf..844fafa 100644 --- a/src/server/controllers/formValidation.js +++ b/src/server/controllers/formValidation.js @@ -20,4 +20,23 @@ export default { })); }, }, + + userForgetPassword: { + 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, + }); + } + })); + }, + }, }; diff --git a/src/server/routes/api.js b/src/server/routes/api.js index a7a75cf..5b7ebdf 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -71,6 +71,10 @@ export default ({ app }) => { bodyParser.json, formValidationController.userRegister.email ); + app.post('/api/forms/userForgetPassword/fields/email/validation', + bodyParser.json, + formValidationController.userForgetPassword.email + ); // locale app.get('/api/locales/:locale', localeController.show); From 3a71ff287566721ccc7841fed1c2c74fb39a325a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sat, 29 Oct 2016 17:39:07 +0800 Subject: [PATCH 06/17] Add reset password token --- configs/project/server.js | 4 ++++ src/server/models/User.js | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/configs/project/server.js b/configs/project/server.js index cfba7ea..d4db489 100755 --- a/configs/project/server.js +++ b/configs/project/server.js @@ -22,6 +22,10 @@ if (process.env.TRAVIS) { secret: 'df5s6sdHdjJdRg56', expiresIn: 60 * 60, // in seconds }, + resetPassword: { + secret: 'FsgWqLhX0Z6JvJfPYwPZ', + expiresIn: 60 * 60, // in seconds + }, }, mongo: require('./mongo/credential'), firebase: require('./firebase/credential.json'), diff --git a/src/server/models/User.js b/src/server/models/User.js index 4a250c6..c9fa246 100644 --- a/src/server/models/User.js +++ b/src/server/models/User.js @@ -48,6 +48,9 @@ let UserSchema = new mongoose.Schema({ linkedin: Object, }, }, + nonce: { + password: Number, + }, lastLoggedInAt: Date, }, { versionKey: false, @@ -74,6 +77,17 @@ UserSchema.methods.toVerifyEmailToken = function(cb) { return token; }; +UserSchema.methods.toResetPasswordToken = function(cb) { + const user = { + _id: this._id, + nonce: this.nonce.password, + }; + const token = jwt.sign(user, configs.jwt.resetPassword.secret, { + expiresIn: configs.jwt.resetPassword.expiresIn, + }); + return token; +}; + UserSchema.methods.toAuthenticationToken = function(cb) { const user = { _id: this._id, From c1d93417b92a5c195874c53741028d934c17e1c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sat, 29 Oct 2016 18:14:00 +0800 Subject: [PATCH 07/17] Add email component ResetPasswordMail --- src/server/components/ResetPasswordMail.js | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/server/components/ResetPasswordMail.js diff --git a/src/server/components/ResetPasswordMail.js b/src/server/components/ResetPasswordMail.js new file mode 100644 index 0000000..47e4e6d --- /dev/null +++ b/src/server/components/ResetPasswordMail.js @@ -0,0 +1,31 @@ +import React, { PropTypes } from 'react'; +import tokenToURL from '../utils/tokenToURL'; + +let ResetPasswordMail = ({ requestedAt, token }) => { + let url = tokenToURL('/user/password/reset', token); + + return ( +
+

+ Someone requested to reset your password at + {' '}. + If you didn't ask for such request, please ignore this mail. +

+

+ Please follow the link to reset your email: +

+

+ + {url} + +

+
+ ); +}; + +ResetPasswordMail.propTypes = { + requestedAt: PropTypes.instanceOf(Date), + token: PropTypes.string, +}; + +export default ResetPasswordMail; From 543e3fb541bd62486079c60a442b61b3a103a435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sat, 29 Oct 2016 18:44:36 +0800 Subject: [PATCH 08/17] Add controller User#setNonce --- src/server/controllers/user.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/server/controllers/user.js b/src/server/controllers/user.js index 7733563..de2db78 100644 --- a/src/server/controllers/user.js +++ b/src/server/controllers/user.js @@ -95,6 +95,18 @@ export default { })); }, + setNonce: (nonceKey) => (req, res, next) => { + User.findOne({ + 'email.value': req.body.email, + }, handleDbError(res)((user) => { + user.nonce[nonceKey] = Math.random(); + user.save(handleDbError(res)((user) => { + req.user = user; + next(); + })); + })); + }, + socialLogin(req, res, next) { let { user } = req; if (!user) { From d5970731f5c0af534de5842a2620d25e7af888a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sat, 29 Oct 2016 18:15:20 +0800 Subject: [PATCH 09/17] Add API User#PasswordRequestReset --- src/common/api/user.js | 3 +++ src/server/controllers/mail.js | 31 +++++++++++++++++++++++++++++++ src/server/routes/api.js | 7 +++++++ 3 files changed, 41 insertions(+) diff --git a/src/common/api/user.js b/src/common/api/user.js index ef1bbab..1121a53 100644 --- a/src/common/api/user.js +++ b/src/common/api/user.js @@ -5,6 +5,9 @@ export default (apiEngine) => ({ data: { verifyEmailToken: token }, }), login: (user) => apiEngine.post('/api/users/login', { data: user }), + requestResetPassword: (form) => ( + apiEngine.post('/api/users/password/request-reset', { data: form }) + ), logout: () => apiEngine.get('/api/users/logout'), read: () => apiEngine.get('/api/users/me'), update: (user) => apiEngine.put('/api/users/me', { data: user }), diff --git a/src/server/controllers/mail.js b/src/server/controllers/mail.js index ca7bbf7..a4de66a 100644 --- a/src/server/controllers/mail.js +++ b/src/server/controllers/mail.js @@ -3,6 +3,7 @@ import { renderToString } from 'react-dom/server'; import Errors from '../../common/constants/Errors'; import nodemailerAPI from '../api/nodemailer'; import VerifyEmailMail from '../components/VerifyEmailMail'; +import ResetPasswordMail from '../components/ResetPasswordMail'; export default { sendVerification(req, res) { @@ -32,4 +33,34 @@ export default { }); }); }, + + sendResetPasswordLink(req, res) { + let { user } = req; + let token = user.toResetPasswordToken(); + + nodemailerAPI() + .sendMail({ + ...( + process.env.NODE_ENV === 'production' ? + { to: user.email.value } : + {} + ), + subject: 'Reset Password Request', + html: renderToString( + + ), + }) + .catch((err) => { + res.errors([Errors.SEND_EMAIL_FAIL]); + throw err; + }) + .then((info) => { + res.json({ + email: info.envelope, + }); + }); + }, }; diff --git a/src/server/routes/api.js b/src/server/routes/api.js index 5b7ebdf..c9ffa76 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -34,6 +34,13 @@ export default ({ app }) => { userController.verifyEmail ); app.post('/api/users/login', bodyParser.json, userController.login); + app.post('/api/users/password/request-reset', + bodyParser.json, + validate('user/ForgetPasswordForm'), + verifyRecaptcha, + userController.setNonce('password'), + mailController.sendResetPasswordLink + ); app.get('/api/users/logout', userController.logout); app.get('/api/users/me', authRequired, userController.show); app.put('/api/users/me', From bd7cb5946d2c7a4a424f4486ee29932b6ea69525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sat, 29 Oct 2016 18:18:55 +0800 Subject: [PATCH 10/17] Add User#ForgetPasswordForm --- .../forms/user/ForgetPasswordForm.js | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/common/components/forms/user/ForgetPasswordForm.js diff --git a/src/common/components/forms/user/ForgetPasswordForm.js b/src/common/components/forms/user/ForgetPasswordForm.js new file mode 100644 index 0000000..a131f00 --- /dev/null +++ b/src/common/components/forms/user/ForgetPasswordForm.js @@ -0,0 +1,115 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Link } from 'react-router'; +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 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('userForgetPassword', 'email', values.email)) + .then((json) => { + let validationError = {}; + if (!json.isPassed) { + validationError.email = json.message; + throw validationError; + } + }); +}; + +class ForgetPasswordForm extends Component { + constructor() { + super(); + this.handleSubmit = this._handleSubmit.bind(this); + } + + _handleSubmit(formData) { + let { dispatch, apiEngine, initialize } = this.props; + + return userAPI(apiEngine) + .requestResetPassword(formData) + .catch((err) => { + dispatch(pushErrors(err)); + throw err; + }) + .then((json) => { + initialize({ + email: '', + }); + }); + } + + render() { + const { + handleSubmit, + submitSucceeded, + submitFailed, + error, + pristine, + submitting, + invalid, + } = this.props; + + return ( +
+ {submitSucceeded && ( + A reset link is sent + )} + {submitFailed && error && ({error})} + + + + + + + + + + ); + } +}; + +export default reduxForm({ + form: 'userForgetPassword', + validate, + asyncValidate, + asyncBlurFields: ['email'], +})(connect(state => ({ + apiEngine: state.apiEngine, +}))(ForgetPasswordForm)); From e905e3b58b3c60cdf7068af12834ac2703c7a468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sat, 29 Oct 2016 18:20:38 +0800 Subject: [PATCH 11/17] Add User#ForgetPasswordPage --- src/common/components/forms/user/LoginForm.js | 4 ++++ .../components/pages/user/ForgetPasswordPage.js | 13 +++++++++++++ src/common/routes/user/forgetPassword.js | 11 +++++++++++ src/common/routes/user/index.js | 1 + 4 files changed, 29 insertions(+) create mode 100644 src/common/components/pages/user/ForgetPasswordPage.js create mode 100644 src/common/routes/user/forgetPassword.js diff --git a/src/common/components/forms/user/LoginForm.js b/src/common/components/forms/user/LoginForm.js index c17e489..e7852f9 100644 --- a/src/common/components/forms/user/LoginForm.js +++ b/src/common/components/forms/user/LoginForm.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; +import { Link } from 'react-router'; import { push } from 'react-router-redux'; import { Field, reduxForm, SubmissionError } from 'redux-form'; import Alert from 'react-bootstrap/lib/Alert'; @@ -100,6 +101,9 @@ class LoginForm extends Component { + + + ); diff --git a/src/common/components/pages/user/ForgetPasswordPage.js b/src/common/components/pages/user/ForgetPasswordPage.js new file mode 100644 index 0000000..6633a36 --- /dev/null +++ b/src/common/components/pages/user/ForgetPasswordPage.js @@ -0,0 +1,13 @@ +import React from 'react'; +import PageHeader from 'react-bootstrap/lib/PageHeader'; +import PageLayout from '../../layouts/PageLayout'; +import ForgetPasswordForm from '../../forms/user/ForgetPasswordForm'; + +let ForgetPasswordPage = () => ( + + Forget Password + + +); + +export default ForgetPasswordPage; diff --git a/src/common/routes/user/forgetPassword.js b/src/common/routes/user/forgetPassword.js new file mode 100644 index 0000000..b955be6 --- /dev/null +++ b/src/common/routes/user/forgetPassword.js @@ -0,0 +1,11 @@ +export default (store) => ({ + path: 'password/forget', + getComponent(nextState, cb) { + require.ensure([], (require) => { + cb( + null, + require('../../components/pages/user/ForgetPasswordPage').default + ); + }); + }, +}); diff --git a/src/common/routes/user/index.js b/src/common/routes/user/index.js index a64bf80..c37082e 100644 --- a/src/common/routes/user/index.js +++ b/src/common/routes/user/index.js @@ -7,6 +7,7 @@ export default (store) => ({ require('./verifyEmail').default(store), require('./login').default(store), require('./edit').default(store), + require('./forgetPassword').default(store), require('./logout').default(store), require('./me').default(store), ]); From 475ad8c36a8a4f46bb505fa5d2b0b331ab9a4d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sun, 30 Oct 2016 01:35:24 +0800 Subject: [PATCH 12/17] Update middleware validate --- src/server/middlewares/validate.js | 36 ++++++++++++++++-------------- src/server/routes/api.js | 6 ++--- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/server/middlewares/validate.js b/src/server/middlewares/validate.js index 67b07e1..ffe101e 100644 --- a/src/server/middlewares/validate.js +++ b/src/server/middlewares/validate.js @@ -1,22 +1,24 @@ import Errors from '../../common/constants/Errors'; -export default (formPath, onlyFields = []) => (req, res, next) => { - let { validate } = require(`../../common/components/forms/${formPath}`); - let errors = validate(req.body); +export default { + form: (formPath, onlyFields = []) => (req, res, next) => { + let { validate } = require(`../../common/components/forms/${formPath}`); + let errors = validate(req.body); - if (onlyFields.length > 0) { - let newErrors = {}; - onlyFields.forEach((field) => { - newErrors[field] = errors[field]; - }); - errors = newErrors; - } + if (onlyFields.length > 0) { + let newErrors = {}; + onlyFields.forEach((field) => { + newErrors[field] = errors[field]; + }); + errors = newErrors; + } - if (Object.keys(errors).length > 0) { - res.pushError(Errors.INVALID_DATA, { - errors, - }); - return res.errors(); - } - next(); + if (Object.keys(errors).length > 0) { + res.pushError(Errors.INVALID_DATA, { + errors, + }); + return res.errors(); + } + next(); + }, }; diff --git a/src/server/routes/api.js b/src/server/routes/api.js index c9ffa76..9b7a6f4 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -36,7 +36,7 @@ export default ({ app }) => { app.post('/api/users/login', bodyParser.json, userController.login); app.post('/api/users/password/request-reset', bodyParser.json, - validate('user/ForgetPasswordForm'), + validate.form('user/ForgetPasswordForm'), verifyRecaptcha, userController.setNonce('password'), mailController.sendResetPasswordLink @@ -46,7 +46,7 @@ export default ({ app }) => { app.put('/api/users/me', authRequired, bodyParser.json, - validate('user/EditForm'), + validate.form('user/EditForm'), userController.update ); app.put('/api/users/me/avatarURL', @@ -57,7 +57,7 @@ export default ({ app }) => { app.put('/api/users/me/password', authRequired, bodyParser.json, - validate('user/ChangePasswordForm'), + validate.form('user/ChangePasswordForm'), userController.updatePassword ); if (configs.firebase) { From c469aa1a575e26e9a324e621688eee3e6a2fc196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sun, 30 Oct 2016 03:27:39 +0800 Subject: [PATCH 13/17] Add middleware validate.verifyUserNonce --- src/server/middlewares/validate.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/server/middlewares/validate.js b/src/server/middlewares/validate.js index ffe101e..11fcd4d 100644 --- a/src/server/middlewares/validate.js +++ b/src/server/middlewares/validate.js @@ -1,4 +1,6 @@ import Errors from '../../common/constants/Errors'; +import { handleDbError } from '../decorators/handleError'; +import User from '../models/User'; export default { form: (formPath, onlyFields = []) => (req, res, next) => { @@ -21,4 +23,16 @@ export default { } next(); }, + + verifyUserNonce: (nonceKey) => (req, res, next) => { + let { _id, nonce } = req.decodedPayload; + User.findById(_id, handleDbError(res)((user) => { + if (nonce !== user.nonce[nonceKey]) { + return res.errors([Errors.TOKEN_REUSED]); + } + user.nonce[nonceKey] = -1; + req.user = user; + next(); + })); + }, }; From 087a62f4e3af03d40014ff784b71c42ea9dd8b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sun, 30 Oct 2016 03:28:52 +0800 Subject: [PATCH 14/17] Add controller User#ResetPassword --- src/server/controllers/user.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/server/controllers/user.js b/src/server/controllers/user.js index de2db78..11b91d5 100644 --- a/src/server/controllers/user.js +++ b/src/server/controllers/user.js @@ -1,8 +1,7 @@ import assign from 'object-assign'; -import jwt from 'jsonwebtoken'; import configs from '../../../configs/project/server'; import Errors from '../../common/constants/Errors'; -import { handleDbError, handleJwtError } from '../decorators/handleError'; +import { handleDbError } from '../decorators/handleError'; import User from '../models/User'; import filterAttribute from '../utils/filterAttribute'; import { loginUser } from '../../common/actions/userActions'; @@ -197,6 +196,20 @@ export default { })); }, + resetPassword(req, res) { + let { user } = req; + let modifiedUser = { + password: req.body.newPassword, + }; + assign(user, modifiedUser); + user.save(handleDbError(res)((user) => { + res.json({ + originAttributes: req.body, + user: user, + }); + })); + }, + uploadAvatar(req, res) { // use `req.file` to access the avatar file // and use `req.body` to access other fileds From 3e90899ec905cc0d7f754fae119d719e4a634223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sun, 30 Oct 2016 03:30:14 +0800 Subject: [PATCH 15/17] Add API User#ResetPassword --- src/common/api/user.js | 8 ++++++++ src/server/routes/api.js | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/common/api/user.js b/src/common/api/user.js index 1121a53..1fecfbf 100644 --- a/src/common/api/user.js +++ b/src/common/api/user.js @@ -8,6 +8,14 @@ export default (apiEngine) => ({ requestResetPassword: (form) => ( apiEngine.post('/api/users/password/request-reset', { data: form }) ), + resetPassword: ({ token, ...form }) => ( + apiEngine.put('/api/users/password', { + data: { + resetPasswordToken: token, + ...form, + }, + }) + ), logout: () => apiEngine.get('/api/users/logout'), read: () => apiEngine.get('/api/users/me'), update: (user) => apiEngine.put('/api/users/me', { data: user }), diff --git a/src/server/routes/api.js b/src/server/routes/api.js index 9b7a6f4..233b643 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -41,6 +41,16 @@ export default ({ app }) => { userController.setNonce('password'), mailController.sendResetPasswordLink ); + app.put('/api/users/password', + bodyParser.json, + bodyParser.jwt( + 'resetPasswordToken', + configs.jwt.resetPassword.secret + ), + validate.verifyUserNonce('password'), + validate.form('user/ResetPasswordForm'), + userController.resetPassword + ); app.get('/api/users/logout', userController.logout); app.get('/api/users/me', authRequired, userController.show); app.put('/api/users/me', From d5d0ade4e0acc78e8cf293e5793a2427100362f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Sun, 30 Oct 2016 03:31:07 +0800 Subject: [PATCH 16/17] Add User#ResetPasswordForm --- .../forms/user/ResetPasswordForm.js | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/common/components/forms/user/ResetPasswordForm.js diff --git a/src/common/components/forms/user/ResetPasswordForm.js b/src/common/components/forms/user/ResetPasswordForm.js new file mode 100644 index 0000000..a85d9cd --- /dev/null +++ b/src/common/components/forms/user/ResetPasswordForm.js @@ -0,0 +1,114 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Link } from 'react-router'; +import { Field, reduxForm } from 'redux-form'; +import Alert from 'react-bootstrap/lib/Alert'; +import Button from 'react-bootstrap/lib/Button'; +import userAPI from '../../../api/user'; +import { pushErrors } from '../../../actions/errorActions'; +import { Form, FormField, FormFooter } from '../../utils/BsForm'; + +export const validate = (values) => { + const errors = {}; + + if ( + values.newPasswordConfirm && + values.newPassword !== values.newPasswordConfirm + ) { + errors.newPassword = errors.newPasswordConfirm = 'Password Not Matched'; + } + + if (!values.newPassword) { + errors.newPassword = 'Required'; + } + + if (!values.newPasswordConfirm) { + errors.newPasswordConfirm = 'Required'; + } + + return errors; +}; + +class ChangePasswordForm extends Component { + constructor(props) { + super(props); + this.handleSubmit = this._handleSubmit.bind(this); + } + + _handleSubmit(formData) { + let { dispatch, apiEngine, routing, initialize } = this.props; + let location = routing.locationBeforeTransitions; + + return userAPI(apiEngine) + .resetPassword({ + ...formData, + token: location.query.token, + }) + .catch((err) => { + dispatch(pushErrors(err)); + throw err; + }) + .then((json) => { + initialize({ + newPassword: '', + newPasswordConfirm: '', + }); + }); + } + + render() { + const { + handleSubmit, + submitSucceeded, + submitFailed, + error, + pristine, + submitting, + invalid, + } = this.props; + + return ( +
+ {submitSucceeded && ( + + Password Changed. + Go to login page now. + + )} + {submitFailed && error && ({error})} + + + +