From 8b536746c35ce4c9740329760263ad918e3525ee Mon Sep 17 00:00:00 2001 From: John Kwening Date: Tue, 13 Mar 2018 22:18:45 -0400 Subject: [PATCH] refactor: Simplify account form validation logic Renamed `~/client/src/js/utilties.js` to 'validate-account-form.js' since the logic in that module were primarily handling submit validation for forms in AccountForm component and processing the request to backend server. Refactored the module to simplify the logic and ease of maintenance. The module had a broad range of functions; not just handling form submit but also handling AccountForm component state. AccountForm state is now handled within its on module to ensure continual encapsulation within its own module and scope. Message is currently handled with alerts. Enhanced style of reset-password view. TODO: - Add logic for updating user information on server - Add custom component for alerts --- client/src/components/AccountForm/index.js | 122 ++++++++------- client/src/components/ProfileCard/index.js | 2 +- client/src/js/server-requests-utils.js | 15 -- client/src/js/utilities.js | 129 ---------------- client/src/js/validate-account-form.js | 167 +++++++++++++++++++++ client/src/routes/profile/index.js | 2 +- client/src/routes/signout/index.js | 2 +- controllers/users-controller.js | 57 ++++--- routes/search.js | 1 + routes/users.js | 8 +- server.js | 2 +- 11 files changed, 276 insertions(+), 231 deletions(-) delete mode 100644 client/src/js/utilities.js create mode 100644 client/src/js/validate-account-form.js diff --git a/client/src/components/AccountForm/index.js b/client/src/components/AccountForm/index.js index 79a0ffc..55bb3a6 100644 --- a/client/src/components/AccountForm/index.js +++ b/client/src/components/AccountForm/index.js @@ -1,6 +1,6 @@ import { h, Component, render } from 'preact'; import style from './style.css'; -import { handleSubmit, clearForms, setStateUserOrRedirectToSignIn } from "../../js/utilities"; +import { validateAccountForm, clearForms } from "../../js/validate-account-form"; import { LOGIN_PATH, REGISTER_PATH, RESET_PATH } from '../../../config'; import linkState from "linkstate"; import { route } from 'preact-router'; @@ -8,102 +8,101 @@ import { route } from 'preact-router'; export default class AccountForm extends Component { constructor() { super(); - this.state = { - form_message: "", - successMessageMap: this.createMessageMap(), - matchPasswordsMap: this.createMatchPasswordsMap(), - validatePasswordMap: this.createValidatePasswordMap(), - }; - this.handleSubmit = handleSubmit.bind(this); - this.routeToRegister = this.routeToRegister.bind(this); - } - createMessageMap = () => { - const messageMap = new Map; - messageMap.set(LOGIN_PATH, 'You have signed in.'); - messageMap.set(REGISTER_PATH, 'You have created an account.'); - messageMap.set(RESET_PATH, 'You have changed your password.'); - return messageMap; + this.routeToRegister = this.routeToRegister.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); } - createMatchPasswordsMap = () => { - const matchPasswordsMap = new Map; - matchPasswordsMap.set(REGISTER_PATH, ['password','confirm_password']); - matchPasswordsMap.set(RESET_PATH, ['new_password','confirm_password']); - return matchPasswordsMap; - } + /** + * TODO - refactor: simplify + * + * Pass: event, path + * Return: a promise - request result that can be used to here to set 'successMessage' + * via setState + */ + handleSubmit = (event) => { + event.preventDefault(); - createValidatePasswordMap = () => { - const validatePasswordMap = new Map; - validatePasswordMap.set(LOGIN_PATH, 'password'); - validatePasswordMap.set(REGISTER_PATH, 'password'); - validatePasswordMap.set(RESET_PATH, 'new_password'); - return validatePasswordMap; - } + const formData = { + name: this.state.name, + email: this.state.email, + password: this.state.password, + new_password: this.state.new_password, + confirm_password: this.state.confirm_password, + } - doSubmit = (event) => { const args = { - event: event, path: this.props.path, - message_key: 'form_message', - component: this, - matchPasswordFields: this.state.matchPasswordsMap.get(this.props.path), - passwordToValidate: this.state.validatePasswordMap.get(this.props.path), - successMessage: this.getSuccessMessage(), - }; - this.handleSubmit(args); - } + formData, + } - getSuccessMessage = () => { - return this.state.successMessageMap.get(this.props.path); - } + validateAccountForm(args).then((response) => { + console.log('validateAccountForm(): ', response); + if (response.status) { + if (this.props.path === RESET_PATH) { + // TODO - alert with success + console.log('path: ', this.props.path); + alert(response.message); + } else { // redirect to /profile with success for login/registration + console.log('route to profile'); + route(`/profile`, true); + } + } else { + // TODO - alert failure to process + alert(response.message); + } + }).catch(function (error) { + alert(error); + }); + }; componentWillUnmount = () => { clearForms(); } componentDidMount = () => { - if (this.props.path === RESET_PATH) { - setStateUserOrRedirectToSignIn(this); - } + // TODO - remove + console.log('AccountForm.componentDidMount()'); } routeToRegister() { route("/register", true); } - render({path},{ form_message, user, name, email, password, new_password, confirm_password }) { + render({ path },{ name, email, password, new_password, confirm_password }) { //DEFAULT TO LOGIN_PATH let display =
-
- + + + value={password} onInput={linkState(this, 'password')} required/>
-
- OR -
-
+
+
+ OR +
+ +

forgot password?

; if(path === REGISTER_PATH){ display =
-
+ - + + value={password} onInput={linkState(this, 'password')} required/> @@ -114,7 +113,7 @@ export default class AccountForm extends Component { if(path === RESET_PATH){ display =
- +

To change user info:

@@ -122,7 +121,7 @@ export default class AccountForm extends Component { value={email} onInput={linkState(this, 'email')}/> -
+

To change password:

@@ -137,7 +136,6 @@ export default class AccountForm extends Component { return (
Navi logo -
{form_message}
{display}
); diff --git a/client/src/components/ProfileCard/index.js b/client/src/components/ProfileCard/index.js index fa2ef9a..933d1da 100644 --- a/client/src/components/ProfileCard/index.js +++ b/client/src/components/ProfileCard/index.js @@ -1,6 +1,6 @@ import {h, Component} from 'preact'; import style from './style'; -import {setStateUserOrRedirectToSignIn} from "../../js/utilities"; +import {setStateUserOrRedirectToSignIn} from "../../js/validate-account-form"; export default class ProfileCard extends Component { constructor() { diff --git a/client/src/js/server-requests-utils.js b/client/src/js/server-requests-utils.js index 507c63f..4202e0b 100644 --- a/client/src/js/server-requests-utils.js +++ b/client/src/js/server-requests-utils.js @@ -98,18 +98,3 @@ export const makeRequest = (method='GET', baseEndPoint, endPointAddon='', bodyDa return axios.request(config); } - - -/* -Incomplete Function... - -import { url } from 'inspector'; - -const AXIOS_INSTANCE = axios.create({ - baseURL: API_SERVER -}); - -exports.postAutocomplete = (input='') => { //ERR: url is read only - // return AXIOS_INSTANCE.post(url=BASE_ENDPOINTS.autocomplete, {input}); -} - */ \ No newline at end of file diff --git a/client/src/js/utilities.js b/client/src/js/utilities.js deleted file mode 100644 index 475283e..0000000 --- a/client/src/js/utilities.js +++ /dev/null @@ -1,129 +0,0 @@ -import {route} from "preact-router"; -import {makeRequest, token} from './server-requests-utils'; - -const getSignInPromise = () => { - return makeRequest('GET','user'); -} - -const formDataForAxios = (form) => { - let formData = {}; - for (var pair of new FormData(form).entries()) { - if(!pair[1]) return null; - formData[pair[0]] = pair[1]; - } - return formData; -} - -const setStateValue = (key, value, component) => { - const stateObject = {}; - stateObject[key] = value; - component.setState(stateObject); -} - -const validEmail = (email) => { - const re = /^(([^<>()\[\]\\.,;:\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,}))$/; - return re.test(String(email).toLowerCase()); -}; - -const validPassword = (password) => { - const re = /^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{6,}$/; - return re.test(String(password)); -}; - -const passwordsMatch = (password1, password2) => { - return password1 === password2; -} - -/** - * Checks whether any of the fields are empty - * Checks whether confirm_password matches password - * Checks whether password is of minimum 6 characters & that it has atleast one number, - * one letter, & atleast one specail character. - * Checks whether the email address is of a valid format - */ -const formIsValid = (args) => { - let {message_key, formData, component, matchPasswordFields, passwordToValidate} = args; - - if(!formData){ - setStateValue(message_key, 'One or more fields were left empty', component); - return false; - } - - if(matchPasswordFields){ - const [password1, password2] = matchPasswordFields; - if (!passwordsMatch(formData[password1],formData[password2])) { - setStateValue(message_key, 'Passwords do no match.', component); - return false; - } - } - - if (!validPassword(formData[passwordToValidate])) { - setStateValue(message_key, - 'Password should have minimum length of 6 & it should have atleast one letter, one number, and one special character', component); - return false; - } - - if (!validEmail(formData.email)) { - setStateValue(message_key, 'Email is not of the valid format', component); - return false; - } - - return true; -} - -// Exports below -export const handleSubmit = (args) => { - let {event, path, message_key, component, matchPasswordFields, passwordToValidate, successMessage} = args; - - event.preventDefault(); - - const formData = formDataForAxios(event.target); - const validationArgs = { - message_key: message_key, - formData: formData, - component: component, - matchPasswordFields: matchPasswordFields, - passwordToValidate: passwordToValidate, - }; - if(!formIsValid(validationArgs)) return; - - makeRequest('POST', path, '', formData) - .then(function (response) { - token.setCookie(response.data.token); - route(`/profile?success=${successMessage}`,true); - }) - .catch(function (error) { - if (error.response === undefined) { - return setStateValue(message_key, error, component); - } - if (error.response.status === 401) { - token.deleteCookie(); - return setStateValue(message_key, 'Wrong password.', component); - } else { - return setStateValue(message_key, error.response.data, component); - } - }); -} - -export const clearForms = () => { - for (let form of document.getElementsByTagName("form")) { - form.reset(); - } -} - -export const logout = () => { - token.deleteCookie(); -} - -export const setStateUserOrRedirectToSignIn = (component) => { - getSignInPromise() - .then((response) => { - component.setState({ - user: response.data, - isSignedIn: true, - }); - } - ).catch(() => { - route('/signin', true); - }); -} diff --git a/client/src/js/validate-account-form.js b/client/src/js/validate-account-form.js new file mode 100644 index 0000000..c818442 --- /dev/null +++ b/client/src/js/validate-account-form.js @@ -0,0 +1,167 @@ +import {route} from 'preact-router'; +import {makeRequest, token} from './server-requests-utils'; +import { LOGIN_PATH, REGISTER_PATH, RESET_PATH } from '../../config'; +import { resolve } from 'url'; + +const validEmail = (email) => { + const re = /^(([^<>()\[\]\\.,;:\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,}))$/; + return re.test(String(email).toLowerCase()); +}; + +const validPassword = (password) => { + const re = /^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{6,}$/; + return re.test(String(password)); +}; + +const passwordsMatch = (password1, password2) => { + return password1 === password2; +}; + +const validateReset = (formData) => { + const {name, email, password, new_password, confirm_password} = formData; + + // passwords form fields are required so if any is null then user info update + if (password && new_password && confirm_password) { + // validate passwords + if (!validPassword(password) || !validPassword(new_password) || + !validPassword(confirm_password)) { + return { + status: false, + message: 'Password should have atleast 6 characts, should have atleast one letter, number, and special character', + body: null, + }; + } else if (!passwordsMatch(new_password, confirm_password)) { + return { + status: false, + message: 'Try again, new passwords don\'t match!', + body: null, + }; + } else { + return { + status: true, + message: 'Success - valid request!', + body: {password, new_password, confirm_password}, + }; + } + } + + // validate user info has at least on field completed + if (name.length < 1 && email.length < 1) { + return { + status: false, + message: 'Please enter a new name and/or email address!', + body: null, + }; + } + + return { + status: false, + message: 'Success - valid request!', + body: {name, email}, + }; +}; + +const validateLogin = (formData) => { + const {email, password} = formData; + + if (!validPassword(password)) { + return { + status: false, + message: 'Password should have atleast 6 characts, should have atleast one letter, number, and special character', + body: null, + }; + } + + return { + status: true, + message: 'Success - valid request!', + body: {email, password}, + }; +}; + +const validateRegister = (formData) => { + const {name, email, password, confirm_password} = formData; + + if (!validPassword(password) || !validPassword(confirm_password)) { + return { + status: false, + message: 'Password should have atleast 6 characts, should have atleast one letter, number, and special character', + body: null, + }; + } else if (!passwordsMatch(password, confirm_password)) { + return { + status: false, + message: 'Try again, passwords don\'t match!', + body: null, + }; + } else { + return { + status: true, + message: 'Success - valid request!', + body: {name, email, password, confirm_password}, + }; + } +}; + +// Exports below +export const validateAccountForm = (args) => { + + const {path, formData} = args; + let result = null; + + if (path === RESET_PATH) { + result = validateReset(formData); + } + + if (path === REGISTER_PATH) { + result = validateRegister(formData); + } + + if (path == LOGIN_PATH) { + result = validateLogin(formData); + } + + const {status, message, body} = result; + + // return if validation fails + if (!status) { + return new Promise((resolve, reject) => { + resolve({status, message}); + }); + } + + // otherwise process server request + return new Promise((resolve, reject) => { + makeRequest('POST', path, '', body) + .then(function (response) { + token.setCookie(response.data.token); + resolve({status, message}); + }) + .catch(function (error) { + reject(error); + }); + }); +}; + +export const clearForms = () => { + for (let form of document.getElementsByTagName("form")) { + form.reset(); + } +}; + +export const logout = () => { + token.deleteCookie(); +}; + +export const setStateUserOrRedirectToSignIn = (component) => { + return makeRequest('GET','user') + .then((response) => { + component.setState({ + user: response.data, + isSignedIn: true, + }); + } + ).catch(() => { + route('/signin', true); + }); +} diff --git a/client/src/routes/profile/index.js b/client/src/routes/profile/index.js index 3f302af..2cda778 100644 --- a/client/src/routes/profile/index.js +++ b/client/src/routes/profile/index.js @@ -7,7 +7,7 @@ import ProfileEditForm from '../../components/ProfileEditForm'; import ProfileSettingsForm from '../../components/ProfileSettingsForm'; import SavedPinsCard from '../../components/SavedPinsCard'; import SearchHistoryCard from '../../components/SearchHistoryCard'; -import {setStateUserOrRedirectToSignIn} from "../../js/utilities"; +import {setStateUserOrRedirectToSignIn} from "../../js/validate-account-form"; export default class Profile extends Component { diff --git a/client/src/routes/signout/index.js b/client/src/routes/signout/index.js index 817e131..c647f44 100644 --- a/client/src/routes/signout/index.js +++ b/client/src/routes/signout/index.js @@ -1,5 +1,5 @@ import { h, Component } from 'preact'; -import { logout } from "../../js/utilities"; +import { logout } from "../../js/validate-account-form"; import Home from '../home'; export default class SignOut extends Component { diff --git a/controllers/users-controller.js b/controllers/users-controller.js index 95876a2..9b74bb0 100644 --- a/controllers/users-controller.js +++ b/controllers/users-controller.js @@ -54,7 +54,8 @@ exports.registerUser = (appReq, appRes) => { // encrypt password const HASHED_PASSWORD = bcrypt.hashSync(appReq.body.password, 8); - //This is inside User.count so that it does not run before the User.count check finishes + // This is inside User.count so that it does not run before the User.count + // check finishes User.create( { name: appReq.body.name, @@ -103,6 +104,17 @@ exports.getUser = (appReq, appRes) => { ); }; +const checkPassword = (password1, password2) => { + return bcrypt.compareSync(password1, password2); +}; + +const getInvalidPasswordResponse = (appRes) => { + return appRes.status(401).send({ + auth: false, + token: null, + }); +}; + /** * @description Handles user login * @@ -119,13 +131,15 @@ exports.loginUser = (appReq, appRes) => { User.findOne({ email: appReq.body.email }, (err, user) => { if (err) return appRes.status(500).send('Error on the server.'); if (!user) return appRes.status(404).send('No user found.'); - if(!checkPassword(appReq.body.password,user.password)) return getInvalidPasswordResponse(appRes); + if (!checkPassword(appReq.body.password, user.password)) { + return getInvalidPasswordResponse(appRes); + } const token = jwt.sign({ id: user._id }, JWT_KEY, { expiresIn: 86400, }); - appRes.status(200).send({ + return appRes.status(200).send({ auth: true, token, }); @@ -156,19 +170,19 @@ exports.logoutUser = (appReq, appRes) => { * @apiError 404 {request error} User not found. * @apiError 500 {server error} Problem finding user. * - * @param {string} appReq.body.email - email provided by user - * @param {string} appReq.body.password - user provided password - * @param {string} appReq.body.new_password - user provided password + * @param {string} appReq.body.password - current user password + * @param {string} appReq.body.new_password - user provided new password * @param {string} appReq.body.confirm_password - user provided password */ exports.resetPassword = (appReq, appRes) => { - const HASHED_PASSWORD = bcrypt.hashSync(appReq.body.new_password, 8); - User.findOne({ email: appReq.body.email }, (err, user) => { + User.findById(appReq.userId, (err, user) => { if (err) return appRes.status(500).send('Error on the server.'); if (!user) return appRes.status(404).send('No user found.'); - if(!checkPassword(appReq.body.password,user.password)) return getInvalidPasswordResponse(appRes); + if (!checkPassword(appReq.body.password, user.password)) { + return getInvalidPasswordResponse(appRes); + } user.password = HASHED_PASSWORD; user.save(function (err, updatedUser) { @@ -187,13 +201,18 @@ exports.resetPassword = (appReq, appRes) => { }; -const checkPassword = (password1, password2) => { - return bcrypt.compareSync(password1, password2); -} - -const getInvalidPasswordResponse = (appRes) => { - return appRes.status(401).send({ - auth: false, - token: null, - }); -} \ No newline at end of file +/** + * @description Handles updating user info + * + * @api {POST} /users/update + * @apiSuccess 200 {auth: true, token: token} jsonwebtoken. + * @apiError 401 {auth: false, token: null} Invalid password. + * @apiError 404 {request error} User not found. + * @apiError 500 {server error} Problem finding user. + * + * @param {string} appReq.body.name - name provided by user + * @param {string} appReq.body.email - email provided by user + */ +exports.update = (appReq, appRes) => { + appRes.status(200).send('Test123'); +}; diff --git a/routes/search.js b/routes/search.js index 24f337b..7cc2861 100644 --- a/routes/search.js +++ b/routes/search.js @@ -38,6 +38,7 @@ router.get('/places/:id', placeDetails); router.post('/autocomplete', autocomplete); router.get('/textsearch', textSearch); router.post('/textsearch', textSearch); + /** * Saved search history end points */ diff --git a/routes/users.js b/routes/users.js index ac38a55..b65b74d 100644 --- a/routes/users.js +++ b/routes/users.js @@ -9,7 +9,7 @@ const router = express.Router(); */ /** * @description Handle requests to users main end point - * + * * @api {GET} /users * @return {success: false, error: err} Not a valid end point */ @@ -20,10 +20,14 @@ router.get('/', (req, res) => { }); }); +/** + * Valid Users endpoints + */ router.post('/register', usersController.registerUser); router.get('/user', verifyToken, usersController.getUser); router.post('/login', usersController.loginUser); router.get('/logout', usersController.logoutUser); -router.post('/reset-password', usersController.resetPassword); +router.post('/reset-password', verifyToken, usersController.resetPassword); +router.post('/update', verifyToken, usersController.update); module.exports = router; diff --git a/server.js b/server.js index 7839207..59b271b 100644 --- a/server.js +++ b/server.js @@ -9,4 +9,4 @@ server.listen(config.PORT, () => { console.info(`Server is running at ${config.HOST}:${config.PORT}`); }); -module.exports = server; \ No newline at end of file +module.exports = server;