From fe6acc90e419b1112bab28a10491985a46391cdc Mon Sep 17 00:00:00 2001 From: Yining Shi Date: Thu, 16 Mar 2017 18:25:12 -0400 Subject: [PATCH] Adding User Settings View (#325) * added account page showing username and email * change username and email * validate current password and add new password * reject promise with error for reduxForm submit-validation for current password * updated user reducer to handle setting sucess and server side async * warning if there is current password but no new password * fixes logout button * import validate function, fixes logout style --- client/components/Nav.jsx | 5 + client/constants.js | 3 + client/modules/User/actions.js | 17 ++++ .../modules/User/components/AccountForm.jsx | 84 +++++++++++++++++ client/modules/User/pages/AccountView.jsx | 92 +++++++++++++++++++ client/modules/User/pages/LoginView.jsx | 14 +-- client/modules/User/pages/SignupView.jsx | 34 +------ client/modules/User/reducers.js | 2 + client/routes.jsx | 2 + client/styles/components/_nav.scss | 8 +- client/utils/reduxFormUtils.js | 58 ++++++++++++ server/controllers/user.controller.js | 42 +++++++++ server/routes/user.routes.js | 2 + 13 files changed, 317 insertions(+), 46 deletions(-) create mode 100644 client/modules/User/components/AccountForm.jsx create mode 100644 client/modules/User/pages/AccountView.jsx diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index c0f9778a..82b73274 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -136,6 +136,11 @@ class Nav extends React.PureComponent { My sketches +
  • + + My account + +
  • + + +
    +

    My Account

    + + {/*

    Or

    + */} +
    + + ); + } +} + +function mapStateToProps(state) { + return { + initialValues: state.user, // <- initialValues for reduxForm + user: state.user, + previousPath: state.ide.previousPath + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators({ updateSettings }, dispatch); +} + +function asyncValidate(formProps, dispatch, props) { + const fieldToValidate = props.form._active; + if (fieldToValidate) { + const queryParams = {}; + queryParams[fieldToValidate] = formProps[fieldToValidate]; + queryParams.check_type = fieldToValidate; + return axios.get('/api/signup/duplicate_check', { params: queryParams }) + .then((response) => { + if (response.data.exists) { + const error = {}; + error[fieldToValidate] = response.data.message; + throw error; + } + }); + } + return Promise.resolve(true).then(() => {}); +} + +AccountView.propTypes = { + previousPath: PropTypes.string.isRequired +}; + +export default reduxForm({ + form: 'updateAllSettings', + fields: ['username', 'email', 'currentPassword', 'newPassword'], + validate: validateSettings, + asyncValidate, + asyncBlurFields: ['username', 'email', 'currentPassword'] +}, mapStateToProps, mapDispatchToProps)(AccountView); diff --git a/client/modules/User/pages/LoginView.jsx b/client/modules/User/pages/LoginView.jsx index 255d6408..b25e5053 100644 --- a/client/modules/User/pages/LoginView.jsx +++ b/client/modules/User/pages/LoginView.jsx @@ -4,6 +4,7 @@ import { Link, browserHistory } from 'react-router'; import InlineSVG from 'react-inlinesvg'; import { validateAndLoginUser } from '../actions'; import LoginForm from '../components/LoginForm'; +import { validateLogin } from '../../../utils/reduxFormUtils'; // import GithubButton from '../components/GithubButton'; const exitUrl = require('../../../images/exit.svg'); const logoUrl = require('../../../images/p5js-logo.svg'); @@ -67,17 +68,6 @@ function mapDispatchToProps() { }; } -function validate(formProps) { - const errors = {}; - if (!formProps.email) { - errors.email = 'Please enter an email'; - } - if (!formProps.password) { - errors.password = 'Please enter a password'; - } - return errors; -} - LoginView.propTypes = { previousPath: PropTypes.string.isRequired }; @@ -85,5 +75,5 @@ LoginView.propTypes = { export default reduxForm({ form: 'login', fields: ['email', 'password'], - validate + validate: validateLogin }, mapStateToProps, mapDispatchToProps)(LoginView); diff --git a/client/modules/User/pages/SignupView.jsx b/client/modules/User/pages/SignupView.jsx index d5759957..47278271 100644 --- a/client/modules/User/pages/SignupView.jsx +++ b/client/modules/User/pages/SignupView.jsx @@ -6,6 +6,7 @@ import InlineSVG from 'react-inlinesvg'; import { reduxForm } from 'redux-form'; import * as UserActions from '../actions'; import SignupForm from '../components/SignupForm'; +import { validateSignup } from '../../../utils/reduxFormUtils'; const exitUrl = require('../../../images/exit.svg'); const logoUrl = require('../../../images/p5js-logo.svg'); @@ -78,37 +79,6 @@ function asyncValidate(formProps, dispatch, props) { return Promise.resolve(true).then(() => {}); } -function validate(formProps) { - const errors = {}; - - if (!formProps.username) { - errors.username = 'Please enter a username.'; - } else if (!formProps.username.match(/^.{1,20}$/)) { - errors.username = 'Username must be less than 20 characters.'; - } else if (!formProps.username.match(/^[a-zA-Z0-9._-]{1,20}$/)) { - errors.username = 'Username must only consist of numbers, letters, periods, dashes, and underscores.'; - } - - if (!formProps.email) { - errors.email = 'Please enter an email.'; - } else if (!formProps.email.match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i)) { - errors.email = 'Please enter a valid email address.'; - } - - if (!formProps.password) { - errors.password = 'Please enter a password'; - } - if (!formProps.confirmPassword) { - errors.confirmPassword = 'Please enter a password confirmation'; - } - - if (formProps.password !== formProps.confirmPassword) { - errors.password = 'Passwords must match'; - } - - return errors; -} - function onSubmitFail(errors) { console.log(errors); } @@ -121,7 +91,7 @@ export default reduxForm({ form: 'signup', fields: ['username', 'email', 'password', 'confirmPassword'], onSubmitFail, - validate, + validate: validateSignup, asyncValidate, asyncBlurFields: ['username', 'email'] }, mapStateToProps, mapDispatchToProps)(SignupView); diff --git a/client/modules/User/reducers.js b/client/modules/User/reducers.js index 04220f58..5554a11f 100644 --- a/client/modules/User/reducers.js +++ b/client/modules/User/reducers.js @@ -19,6 +19,8 @@ const user = (state = { authenticated: false }, action) => { return Object.assign({}, state, { resetPasswordInitiate: false }); case ActionTypes.INVALID_RESET_PASSWORD_TOKEN: return Object.assign({}, state, { resetPasswordInvalid: true }); + case ActionTypes.SETTINGS_UPDATED: + return { ...state, ...action.user }; default: return state; } diff --git a/client/routes.jsx b/client/routes.jsx index 50159175..3ceaa713 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -7,6 +7,7 @@ import LoginView from './modules/User/pages/LoginView'; import SignupView from './modules/User/pages/SignupView'; import ResetPasswordView from './modules/User/pages/ResetPasswordView'; import NewPasswordView from './modules/User/pages/NewPasswordView'; +import AccountView from './modules/User/pages/AccountView'; // import SketchListView from './modules/Sketch/pages/SketchListView'; import { getUser } from './modules/User/actions'; @@ -27,6 +28,7 @@ const routes = store => + ); diff --git a/client/styles/components/_nav.scss b/client/styles/components/_nav.scss index 84f91fdd..2eaaccf4 100644 --- a/client/styles/components/_nav.scss +++ b/client/styles/components/_nav.scss @@ -56,7 +56,7 @@ .nav__item-spacer { @include themify() { color: map-get($theme-map, 'inactive-text-color'); - } + } padding: 0 #{15 / $base-font-size}rem; } @@ -65,12 +65,16 @@ width: 100%; } -.nav__dropdown a { +.nav__dropdown a, button { @include themify() { color: getThemifyVariable('secondary-text-color'); } } +.nav__dropdown button { + padding: 0; +} + .nav__dropdown a:hover { @include themify() { color: getThemifyVariable('primary-text-color'); diff --git a/client/utils/reduxFormUtils.js b/client/utils/reduxFormUtils.js index 8b4b1205..c2c68fa0 100644 --- a/client/utils/reduxFormUtils.js +++ b/client/utils/reduxFormUtils.js @@ -14,3 +14,61 @@ export const domOnlyProps = ({ error, ...domProps }) => domProps; /* eslint-enable */ + +function validateNameEmail(formProps, errors) { + if (!formProps.username) { + errors.username = 'Please enter a username.'; + } else if (!formProps.username.match(/^.{1,20}$/)) { + errors.username = 'Username must be less than 20 characters.'; + } else if (!formProps.username.match(/^[a-zA-Z0-9._-]{1,20}$/)) { + errors.username = 'Username must only consist of numbers, letters, periods, dashes, and underscores.'; + } + + if (!formProps.email) { + errors.email = 'Please enter an email.'; + } else if (!formProps.email.match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i)) { + errors.email = 'Please enter a valid email address.'; + } +} + +export function validateSettings(formProps) { + const errors = {}; + + validateNameEmail(formProps, errors); + + if (formProps.currentPassword && !formProps.newPassword) { + errors.newPassword = 'Please enter a new password or leave the current password empty.'; + } + + return errors; +} + +export function validateLogin(formProps) { + const errors = {}; + if (!formProps.email) { + errors.email = 'Please enter an email'; + } + if (!formProps.password) { + errors.password = 'Please enter a password'; + } + return errors; +} + +export function validateSignup(formProps) { + const errors = {}; + + validateNameEmail(formProps, errors); + + if (!formProps.password) { + errors.password = 'Please enter a password'; + } + if (!formProps.confirmPassword) { + errors.confirmPassword = 'Please enter a password confirmation'; + } + + if (formProps.password !== formProps.confirmPassword) { + errors.password = 'Passwords must match'; + } + + return errors; +} diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index 306f7ee5..ffe6876e 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -182,3 +182,45 @@ export function userExists(username, callback) { user ? callback(true) : callback(false) )); } + +export function updateSettings(req, res) { + User.findById(req.user.id, (err, user) => { + if (err) { + res.status(500).json({ error: err }); + return; + } + if (!user) { + res.status(404).json({ error: 'Document not found' }); + return; + } + + user.email = req.body.email; + user.username = req.body.username; + + if (req.body.currentPassword) { + user.comparePassword(req.body.currentPassword, (err, isMatch) => { + if (err) throw err; + if (!isMatch) { + res.status(401).json({ error: 'Current password is invalid.' }); + return; + } else { + user.password = req.body.newPassword; + saveUser(res, user); + } + }); + } else { + saveUser(res, user); + } + }); +} + +export function saveUser(res, user) { + user.save((saveErr) => { + if (saveErr) { + res.status(500).json({ error: saveErr }); + return; + } + + res.json(user); + }); +} diff --git a/server/routes/user.routes.js b/server/routes/user.routes.js index a295e96c..b3468cd8 100644 --- a/server/routes/user.routes.js +++ b/server/routes/user.routes.js @@ -15,4 +15,6 @@ router.route('/reset-password/:token').get(UserController.validateResetPasswordT router.route('/reset-password/:token').post(UserController.updatePassword); +router.route('/account').put(UserController.updateSettings); + export default router;