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/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
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
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
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/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/pg.js b/db/pg.js
index f27d041..1eda961 100644
--- a/db/pg.js
+++ b/db/pg.js
@@ -7,7 +7,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/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 415efd9..a265184 100644
--- a/db/sql/3-functions.sql
+++ b/db/sql/3-functions.sql
@@ -16,6 +16,24 @@ 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,
+ user_time_password_changed
+ ) = (
+ _new_hash,
+ now()
+ )
+ 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" />
+ Change Password
+
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/pages/account/index.js b/pages/account/index.js
index 27607d9..390c8e8 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'
@@ -40,10 +41,14 @@ export default function AccountPage({orders}) {
Email and Password
-
Email: {user.email} Change
+
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
diff --git a/pages/login.js b/pages/login.js
index dd9cf85..81b71b7 100644
--- a/pages/login.js
+++ b/pages/login.js
@@ -4,6 +4,7 @@ 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'
import {useSetUser} from '~/hooks/useUser'
@@ -11,9 +12,11 @@ import {useSetUser} from '~/hooks/useUser'
export default function Login(){
const setUser = useSetUser()
useAccountRedirect()
+ const setUser = useSetUser()
const redirectAfterLogin = user => {
setUser(user)
+
if (user.is_admin)
Router.push('/admin')
else