From 53dcb3ec862cde67ba1f4d8740637600a91800e9 Mon Sep 17 00:00:00 2001 From: Ashelyn Dawn Date: Tue, 15 Dec 2020 21:00:26 -0700 Subject: [PATCH 1/7] Update node-postgres --- db/pg.js | 3 ++- package-lock.json | 44 ++++++++++++++++++-------------------------- package.json | 2 +- 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/db/pg.js b/db/pg.js index cbcf8bf..be78b88 100644 --- a/db/pg.js +++ b/db/pg.js @@ -6,7 +6,8 @@ const pool = new Pool({ host: process.env.DB_HOST, user: process.env.DB_USER, database: process.env.DB_NAME, - password: process.env.DB_PASS + password: process.env.DB_PASS, + connectionTimeoutMillis: 200 }); pool.on('error', err=>debug(err)); diff --git a/package-lock.json b/package-lock.json index f32d23f..5c86244 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4915,46 +4915,38 @@ } }, "pg": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/pg/-/pg-7.18.2.tgz", - "integrity": "sha512-Mvt0dGYMwvEADNKy5PMQGlzPudKcKKzJds/VbOeZJpb6f/pI3mmoXX0JksPgI3l3JPP/2Apq7F36O63J7mgveA==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.5.1.tgz", + "integrity": "sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw==", "requires": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", - "pg-connection-string": "0.1.3", - "pg-packet-stream": "^1.1.0", - "pg-pool": "^2.0.10", + "pg-connection-string": "^2.4.0", + "pg-pool": "^3.2.2", + "pg-protocol": "^1.4.0", "pg-types": "^2.1.0", - "pgpass": "1.x", - "semver": "4.3.2" - }, - "dependencies": { - "semver": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz", - "integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=" - } + "pgpass": "1.x" } }, "pg-connection-string": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz", - "integrity": "sha1-2hhHsglA5C7hSSvq9l1J2RskXfc=" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz", + "integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==" }, "pg-int8": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" }, - "pg-packet-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pg-packet-stream/-/pg-packet-stream-1.1.0.tgz", - "integrity": "sha512-kRBH0tDIW/8lfnnOyTwKD23ygJ/kexQVXZs7gEyBljw4FYqimZFxnMMx50ndZ8In77QgfGuItS5LLclC2TtjYg==" - }, "pg-pool": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-2.0.10.tgz", - "integrity": "sha512-qdwzY92bHf3nwzIUcj+zJ0Qo5lpG/YxchahxIN8+ZVmXqkahKXsnl2aiJPHLYN9o5mB/leG+Xh6XKxtP7e0sjg==" + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.2.tgz", + "integrity": "sha512-ORJoFxAlmmros8igi608iVEbQNNZlp89diFVx6yV5v+ehmpMY9sK6QgpmgoXbmkNaBAx8cOOZh9g80kJv1ooyA==" + }, + "pg-protocol": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.4.0.tgz", + "integrity": "sha512-El+aXWcwG/8wuFICMQjM5ZSAm6OWiJicFdNYo+VY3QP+8vI4SvLIWVe51PppTzMhikUJR+PsyIFKqfdXPz/yxA==" }, "pg-types": { "version": "2.2.0", diff --git a/package.json b/package.json index 5b48236..6e6635f 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "multer": "^1.4.2", "next": "^10.0.3", "next-images": "^1.6.2", - "pg": "^7.18.2", + "pg": "^8.5.1", "react": "^16.14.0", "react-dom": "^16.14.0", "react-infinite-calendar": "^2.3.1", From 13d9d98f6639b22a33106985c71021aa07f33fc1 Mon Sep 17 00:00:00 2001 From: Ashelyn Dawn Date: Fri, 18 Dec 2020 17:49:44 -0700 Subject: [PATCH 2/7] Fix mailing list link --- components/footer/footer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/footer/footer.js b/components/footer/footer.js index 08582c1..030730f 100644 --- a/components/footer/footer.js +++ b/components/footer/footer.js @@ -21,7 +21,7 @@ const Footer = () => {

© 2015 - {DateTime.utc().year} Society of Socks

From 0d1f1d82b7072962083ebde2005d331c867efc79 Mon Sep 17 00:00:00 2001 From: Ashelyn Dawn Date: Fri, 18 Dec 2020 18:15:16 -0700 Subject: [PATCH 3/7] Fix initial login to not show unset password --- api/auth.js | 4 +--- pages/login.js | 4 ++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/api/auth.js b/api/auth.js index b6884f4..41f8575 100644 --- a/api/auth.js +++ b/api/auth.js @@ -28,9 +28,7 @@ router.post('/', parseJSON, loginValidation, async (req, res) => { await db.session.create(req, user) - const {password_hash, ...result} = user - - res.json(result) + res.json(user) }) // TODO: Login link stuff diff --git a/pages/login.js b/pages/login.js index b37439c..bb5d04f 100644 --- a/pages/login.js +++ b/pages/login.js @@ -4,13 +4,17 @@ import Head from 'next/head' import Router from 'next/router' import isEmail from 'validator/lib/isEmail' +import {useSetUser} from '~/hooks/useUser' import {FormController, Input, Button} from '~/components/form' import useAccountRedirect from '~/hooks/useAccountRedirect' export default function Login(){ useAccountRedirect() + const setUser = useSetUser() const redirectAfterLogin = user => { + setUser(user) + if (user.is_admin) Router.push('/admin') else From c777651ce06e4f6b6c529841758352595cc6125d Mon Sep 17 00:00:00 2001 From: Ashelyn Dawn Date: Fri, 18 Dec 2020 18:32:05 -0700 Subject: [PATCH 4/7] User can change password --- api/middleware/validators.js | 6 +++++ api/users.js | 25 ++++++++++++++++++++ db/models/user.js | 30 ++++++++++++++++++++++++ db/sql/3-functions.sql | 12 ++++++++++ pages/account/change-password.js | 40 ++++++++++++++++++++++++++++++++ pages/account/index.js | 3 ++- 6 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 pages/account/change-password.js 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

From 587b4173d40fdae396000e9e81b7d8e6ff465342 Mon Sep 17 00:00:00 2001 From: Ashelyn Dawn Date: Fri, 18 Dec 2020 18:48:07 -0700 Subject: [PATCH 5/7] Shows user when password last changed --- db/mappings/user.js | 2 ++ db/sql/1-tables.sql | 1 + db/sql/2-views.sql | 1 + db/sql/3-functions.sql | 8 +++++++- pages/account/index.js | 8 ++++++-- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/db/mappings/user.js b/db/mappings/user.js index 554e324..55b1007 100644 --- a/db/mappings/user.js +++ b/db/mappings/user.js @@ -8,6 +8,7 @@ module.exports = [ 'email_confirmed', 'time_registered', 'time_email_confirmed', + 'time_password_changed', 'last_active', 'num_orders', 'is_admin' @@ -21,6 +22,7 @@ module.exports = [ 'email_confirmed', 'time_registered', 'time_email_confirmed', + 'time_password_changed', 'is_admin' ], collections: [ diff --git a/db/sql/1-tables.sql b/db/sql/1-tables.sql index 2eabe9a..3d8ec4c 100644 --- a/db/sql/1-tables.sql +++ b/db/sql/1-tables.sql @@ -9,6 +9,7 @@ create table sos."user" ( user_email citext unique not null, user_email_confirmed boolean not null default false, user_password_hash varchar(60), + user_time_password_changed timestamptz not null default now(), user_time_registered timestamptz not null default now(), user_time_email_confirmed timestamptz, user_is_admin bool not null default false diff --git a/db/sql/2-views.sql b/db/sql/2-views.sql index b227b0c..84ffe4e 100644 --- a/db/sql/2-views.sql +++ b/db/sql/2-views.sql @@ -41,6 +41,7 @@ create or replace view sos.v_session as "session_user".user_password_hash as session_user_password_hash, "session_user".user_time_registered as session_user_time_registered, "session_user".user_time_email_confirmed as session_user_time_email_confirmed, + "session_user".user_time_password_changed as session_user_time_password_changed, "session_user".user_is_admin as session_user_is_admin, v_cart.* from sos."session" diff --git a/db/sql/3-functions.sql b/db/sql/3-functions.sql index 8f3abbc..a265184 100644 --- a/db/sql/3-functions.sql +++ b/db/sql/3-functions.sql @@ -22,7 +22,13 @@ create or replace function sos.change_password(_user_uuid uuid, _new_hash text) as $function$ begin update sos."user" - set user_password_hash = _new_hash + set ( + user_password_hash, + user_time_password_changed + ) = ( + _new_hash, + now() + ) where user_uuid = _user_uuid; return query select * from sos."user" where user_uuid = _user_uuid; diff --git a/pages/account/index.js b/pages/account/index.js index 8ea397d..390c8e8 100644 --- a/pages/account/index.js +++ b/pages/account/index.js @@ -41,10 +41,14 @@ export default function AccountPage({orders}) {

Email and Password

-

Email: {user.email}

+

Email: {user.email} Change

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

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

+

Password: { + !user.password_hash + ? 'Unset' + : <>Last changed {DateTime.fromISO(user.time_password_changed).toFormat('LLLL dd yyyy, h:mm a')}. Change + }

Your Orders

From 0cdd1336de944640f4e2ffcce855a84c4941f970 Mon Sep 17 00:00:00 2001 From: Ashelyn Dawn Date: Fri, 18 Dec 2020 19:45:11 -0700 Subject: [PATCH 6/7] Header styles for small screens --- components/header/style.module.css | 77 ++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/components/header/style.module.css b/components/header/style.module.css index 301fa16..31b5c79 100644 --- a/components/header/style.module.css +++ b/components/header/style.module.css @@ -141,3 +141,80 @@ h1.title a:hover { .container ul li a:hover { border-bottom: solid 1px black; } + + +@media (max-width: 900px) { + .logo { + display: none; + } + + h1.title { + margin-top: 0; + text-align: left; + position: initial; + padding-top: 10px; + margin-bottom: 0; + margin-left: 20px; + width: auto; + } + + ul.primaryNav { + position: initial; + float: none; + width: 383px; + margin-left: 20px; + margin-top: 0; + } +} + +@media (max-width: 650px) { + .container { + height: 75px; + } + + h1.title { + font-size: 20px; + position: relative; + top: 0; + left: 0; + right: 0; + bottom: 0; + height: auto; + } + + ul.primaryNav { + width: auto; + margin-left: 0; + padding-bottom: 10px; + margin-top: 16px; + } +} + +@media (max-width: 500px) { + .container { + height: auto; + } + + h1.title { + font-size: 20px; + position: relative; + top: 0; + left: 0; + right: 0; + bottom: 0; + height: auto; + width: 100%; + text-align: center; + margin: 0; + } + + ul.accountNav { + float: none; + width: 100%; + text-align: center; + position: relative; + top: -10px; + right: 0; + padding-left: 0; + } +} \ No newline at end of file From faa87d6f09df481112344842c6643b210ec835ca Mon Sep 17 00:00:00 2001 From: Ashelyn Dawn Date: Fri, 18 Dec 2020 19:54:02 -0700 Subject: [PATCH 7/7] Hero styles for small screens --- components/hero/style.module.css | 40 ++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/components/hero/style.module.css b/components/hero/style.module.css index 83927f3..f1cac99 100644 --- a/components/hero/style.module.css +++ b/components/hero/style.module.css @@ -6,33 +6,29 @@ } .hero > div { - margin: 0; - margin-right: 0px; - margin-left: 0px; + margin: 0 auto; max-width: 2000px; - margin-left: auto; - margin-right: auto; - padding: 0; + padding: 15px; position: relative; - top: 0; background-size: cover; background-position: center center; min-height: 400px; box-shadow: inset 0 -15px 7.5px -7.5px rgba(0,0,0,0.2); + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; } -.hero img.icon { - position: absolute; - right: 60%; +.icon { margin-right: 20px; - margin-top: -10px; - top: 80px; max-width: 300px; - + height: auto; + display: block; } -.hero div.content { - position: absolute; +.content { + max-width: 400px; left: 40%; right: 20%; padding-left: 20px; @@ -49,3 +45,17 @@ .hero p { opacity: .7; } + +@media (max-width: 650px) { + .hero > div { + flex-direction: column; + } + + .icon { + margin-right: 0; + } + + .content { + padding: 0; + } +} \ No newline at end of file