diff --git a/api/middleware/validators.js b/api/middleware/validators.js index 2aaa675..a5f3fa9 100644 --- a/api/middleware/validators.js +++ b/api/middleware/validators.js @@ -11,6 +11,12 @@ validators.bothPasswordsMatch = body('password').custom((pass, {req})=>{ return true }) +validators.oldPasswordNotSame = body('oldPassword').custom((pass, {req}) => { + if(pass === req.body.password) + throw new Error('New password is the same as old password') + return true +}) + validators.validEmail = field => body(field).isString().isEmail() .withMessage('Email invalid') diff --git a/api/users.js b/api/users.js index 6e8e958..ba7f989 100644 --- a/api/users.js +++ b/api/users.js @@ -2,6 +2,7 @@ const router = require('express-promise-router')() const parseJSON = require('body-parser').json() const db = require('../db') const ensureAdmin = require('./middleware/ensureAdmin') +const ensureUser = require('./middleware/ensureUser') const sendgrid = require('@sendgrid/mail') sendgrid.setApiKey(process.env.SENDGRID_KEY) @@ -77,4 +78,28 @@ router.delete('/:uuid/admin', ensureAdmin, async (req, res) => { res.json(user) }) +const changePasswordValidation = [ + validate.validPassword('password'), + validate.bothPasswordsMatch, + validate.oldPasswordNotSame, + validate.handleApiError +] +router.put('/current/password', parseJSON, changePasswordValidation, ensureUser, async (req, res) => { + const user = await db.user.changePassword( + req.user.uuid, + req.body.oldPassword, + req.body.password + ) + + if(!user){ + return res.status(422).json({errors: [{ + param: 'oldPassword', + msg: 'Incorrect password' + }]}) + } + + + res.json(user) +}) + module.exports = router; diff --git a/db/models/user.js b/db/models/user.js index bcc907e..b4316dc 100644 --- a/db/models/user.js +++ b/db/models/user.js @@ -73,6 +73,36 @@ user.login = async (email, password) => { return _user } +user.changePassword = async (user_uuid, oldPassword, newPassword) => { + const _user = await user.findById(user_uuid) + + if(!_user){ + // Avoid early exit timing difference + await bcrypt.hash(oldPassword, saltRounds) + return null + } + + const passwordCorrect = await bcrypt.compare(oldPassword, _user.password_hash) + + if(!passwordCorrect) + return null + + const newHash = await bcrypt.hash(newPassword, saltRounds) + + const query = { + text: 'select * from sos.change_password($1, $2)', + values: [ + user_uuid, + newHash + ] + } + + debug(query); + + const {rows} = await pg.query(query) + return joinjs.map(rows, mappings, 'userMap', 'user_')[0]; +} + user.getOpenEmailLinks = (user_uuid) => dbUtil.executeFunction({ name: 'get_open_email_links_for_user', diff --git a/db/sql/3-functions.sql b/db/sql/3-functions.sql index 415efd9..8f3abbc 100644 --- a/db/sql/3-functions.sql +++ b/db/sql/3-functions.sql @@ -16,6 +16,18 @@ begin return query select * from sos."user" where user_uuid = _user_uuid; end; $function$; +create or replace function sos.change_password(_user_uuid uuid, _new_hash text) + returns setof sos.user + language plpgsql +as $function$ +begin + update sos."user" + set user_password_hash = _new_hash + where user_uuid = _user_uuid; + + return query select * from sos."user" where user_uuid = _user_uuid; +end; $function$; + create or replace function sos.validate_session(_session_uuid uuid) returns setof sos.v_session language plpgsql diff --git a/pages/account/change-password.js b/pages/account/change-password.js new file mode 100644 index 0000000..f480535 --- /dev/null +++ b/pages/account/change-password.js @@ -0,0 +1,40 @@ +import {useSetUser} from '~/hooks/useUser' +import Head from 'next/head' +import Router from 'next/router' +import redirect from '~/utils/redirectGetInitialProps' +import {FormController, Input, Button} from '~/components/form' + +ChangePassword.getInitialProps = async function({ctx, user}) { + if(!user) + return redirect(ctx, 302, '/login') + + if(!user.email_confirmed) + return redirect(ctx, 302, '/account/email/confirm') + + return {} +} + +export default function ChangePassword() { + const setUser = useSetUser() + + function afterChange(user) { + setUser(user); + Router.push('/account') + } + + return ( + <> + + Change Password | Society of Socks + +

Change Password

+ + (value.length >= 8)} hint="Password must be at least 8 characters long" /> + (value.length >= 8)} hint="Password must be at least 8 characters long" /> + (value === fields.password.value)} hint="Passwords must match" /> + + + + + ) +} \ No newline at end of file diff --git a/pages/account/index.js b/pages/account/index.js index 27607d9..8ea397d 100644 --- a/pages/account/index.js +++ b/pages/account/index.js @@ -1,6 +1,7 @@ import {DateTime} from 'luxon' import Head from 'next/head' import Router from 'next/router' +import Link from 'next/link' import Table from '~/components/table' import useUser from '~/hooks/useUser' @@ -43,7 +44,7 @@ export default function AccountPage({orders}) {

Email: {user.email}

{/* TODO: Store date password was set so we can show "Set on [date]"? */} -

Password: {!user.password_hash ? 'Unset' : <>Set. }

+

Password: {!user.password_hash ? 'Unset' : <>Set. Change}

Your Orders