Merge branch 'master' of gitlab.com:pawnstar/sos-nextjs

main
Ashelyn Dawn 4 years ago
commit 99d2a65e17

@ -11,6 +11,12 @@ validators.bothPasswordsMatch = body('password').custom((pass, {req})=>{
return true 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() validators.validEmail = field => body(field).isString().isEmail()
.withMessage('Email invalid') .withMessage('Email invalid')

@ -2,6 +2,7 @@ const router = require('express-promise-router')()
const parseJSON = require('body-parser').json() const parseJSON = require('body-parser').json()
const db = require('../db') const db = require('../db')
const ensureAdmin = require('./middleware/ensureAdmin') const ensureAdmin = require('./middleware/ensureAdmin')
const ensureUser = require('./middleware/ensureUser')
const sendgrid = require('@sendgrid/mail') const sendgrid = require('@sendgrid/mail')
sendgrid.setApiKey(process.env.SENDGRID_KEY) sendgrid.setApiKey(process.env.SENDGRID_KEY)
@ -77,4 +78,28 @@ router.delete('/:uuid/admin', ensureAdmin, async (req, res) => {
res.json(user) 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; module.exports = router;

@ -21,7 +21,7 @@ const Footer = () => {
<p>© 2015 - {DateTime.utc().year} Society of Socks</p> <p>© 2015 - {DateTime.utc().year} Society of Socks</p>
</div> </div>
<ul className={styles.footerNav}> <ul className={styles.footerNav}>
<li><Link href="#mailingList"><a onClick={(ev)=>{if(ev) ev.preventDefault(); console.log('mailing list')}}>Mailing List</a></Link></li> <li><a href="http://eepurl.com/clkzNP" target="_blank">Mailing List</a></li>
<li><Link href="/contact"><a>Contact Us</a></Link></li> <li><Link href="/contact"><a>Contact Us</a></Link></li>
<li><Link href="/privacy"><a>Privacy</a></Link></li> <li><Link href="/privacy"><a>Privacy</a></Link></li>
</ul> </ul>

@ -141,3 +141,80 @@ h1.title a:hover {
.container ul li a:hover { .container ul li a:hover {
border-bottom: solid 1px black; 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;
}
}

@ -6,33 +6,29 @@
} }
.hero > div { .hero > div {
margin: 0; margin: 0 auto;
margin-right: 0px;
margin-left: 0px;
max-width: 2000px; max-width: 2000px;
margin-left: auto; padding: 15px;
margin-right: auto;
padding: 0;
position: relative; position: relative;
top: 0;
background-size: cover; background-size: cover;
background-position: center center; background-position: center center;
min-height: 400px; min-height: 400px;
box-shadow: inset 0 -15px 7.5px -7.5px rgba(0,0,0,0.2); 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 { .icon {
position: absolute;
right: 60%;
margin-right: 20px; margin-right: 20px;
margin-top: -10px;
top: 80px;
max-width: 300px; max-width: 300px;
height: auto;
display: block;
} }
.hero div.content { .content {
position: absolute; max-width: 400px;
left: 40%; left: 40%;
right: 20%; right: 20%;
padding-left: 20px; padding-left: 20px;
@ -49,3 +45,17 @@
.hero p { .hero p {
opacity: .7; opacity: .7;
} }
@media (max-width: 650px) {
.hero > div {
flex-direction: column;
}
.icon {
margin-right: 0;
}
.content {
padding: 0;
}
}

@ -8,6 +8,7 @@ module.exports = [
'email_confirmed', 'email_confirmed',
'time_registered', 'time_registered',
'time_email_confirmed', 'time_email_confirmed',
'time_password_changed',
'last_active', 'last_active',
'num_orders', 'num_orders',
'is_admin' 'is_admin'
@ -21,6 +22,7 @@ module.exports = [
'email_confirmed', 'email_confirmed',
'time_registered', 'time_registered',
'time_email_confirmed', 'time_email_confirmed',
'time_password_changed',
'is_admin' 'is_admin'
], ],
collections: [ collections: [

@ -73,6 +73,36 @@ user.login = async (email, password) => {
return _user 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) => user.getOpenEmailLinks = (user_uuid) =>
dbUtil.executeFunction({ dbUtil.executeFunction({
name: 'get_open_email_links_for_user', name: 'get_open_email_links_for_user',

@ -7,7 +7,8 @@ const pool = new Pool({
host: process.env.DB_HOST, host: process.env.DB_HOST,
user: process.env.DB_USER, user: process.env.DB_USER,
database: process.env.DB_NAME, database: process.env.DB_NAME,
password: process.env.DB_PASS password: process.env.DB_PASS,
connectionTimeoutMillis: 200
}); });
pool.on('error', err=>debug(err)); pool.on('error', err=>debug(err));

@ -9,6 +9,7 @@ create table sos."user" (
user_email citext unique not null, user_email citext unique not null,
user_email_confirmed boolean not null default false, user_email_confirmed boolean not null default false,
user_password_hash varchar(60), user_password_hash varchar(60),
user_time_password_changed timestamptz not null default now(),
user_time_registered timestamptz not null default now(), user_time_registered timestamptz not null default now(),
user_time_email_confirmed timestamptz, user_time_email_confirmed timestamptz,
user_is_admin bool not null default false user_is_admin bool not null default false

@ -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_password_hash as session_user_password_hash,
"session_user".user_time_registered as session_user_time_registered, "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_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, "session_user".user_is_admin as session_user_is_admin,
v_cart.* v_cart.*
from sos."session" from sos."session"

@ -16,6 +16,24 @@ begin
return query select * from sos."user" where user_uuid = _user_uuid; return query select * from sos."user" where user_uuid = _user_uuid;
end; $function$; 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) create or replace function sos.validate_session(_session_uuid uuid)
returns setof sos.v_session returns setof sos.v_session
language plpgsql language plpgsql

@ -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 (
<>
<Head>
<title>Change Password | Society of Socks</title>
</Head>
<h2>Change Password</h2>
<FormController method="put" url="/api/users/current/password" afterSubmit={afterChange}>
<Input label="Current Password" type="password" name="oldPassword" validate={value=>(value.length >= 8)} hint="Password must be at least 8 characters long" />
<Input label="New Password" type="password" name="password" validate={value=>(value.length >= 8)} hint="Password must be at least 8 characters long" />
<Input label="Repeat password" type="password" name="password2" validate={(value, fields)=>(value === fields.password.value)} hint="Passwords must match" />
<Button type="submit">Change Password</Button>
</FormController>
</>
)
}

@ -1,6 +1,7 @@
import {DateTime} from 'luxon' import {DateTime} from 'luxon'
import Head from 'next/head' import Head from 'next/head'
import Router from 'next/router' import Router from 'next/router'
import Link from 'next/link'
import Table from '~/components/table' import Table from '~/components/table'
import useUser from '~/hooks/useUser' import useUser from '~/hooks/useUser'
@ -40,10 +41,14 @@ export default function AccountPage({orders}) {
<h3>Email and Password</h3> <h3>Email and Password</h3>
<div style={{maxWidth: 700, margin: '0 auto'}}> <div style={{maxWidth: 700, margin: '0 auto'}}>
<p><strong>Email:</strong> {user.email} <button className="buttonLink">Change</button></p> <p><strong>Email:</strong> {user.email} <Link href="/account/change-email"><a>Change</a></Link></p>
{/* TODO: Store date password was set so we can show "Set on [date]"? */} {/* TODO: Store date password was set so we can show "Set on [date]"? */}
<p><strong>Password:</strong> {!user.password_hash ? 'Unset' : <>Set. <button className="buttonLink">Change</button></>}</p> <p><strong>Password:</strong> {
!user.password_hash
? 'Unset'
: <>Last changed {DateTime.fromISO(user.time_password_changed).toFormat('LLLL dd yyyy, h:mm a')}. <Link href="/account/change-password"><a>Change</a></Link></>
}</p>
</div> </div>
<h3>Your Orders</h3> <h3>Your Orders</h3>

@ -4,6 +4,7 @@ import Head from 'next/head'
import Router from 'next/router' import Router from 'next/router'
import isEmail from 'validator/lib/isEmail' import isEmail from 'validator/lib/isEmail'
import {useSetUser} from '~/hooks/useUser'
import {FormController, Input, Button} from '~/components/form' import {FormController, Input, Button} from '~/components/form'
import useAccountRedirect from '~/hooks/useAccountRedirect' import useAccountRedirect from '~/hooks/useAccountRedirect'
import {useSetUser} from '~/hooks/useUser' import {useSetUser} from '~/hooks/useUser'
@ -11,9 +12,11 @@ import {useSetUser} from '~/hooks/useUser'
export default function Login(){ export default function Login(){
const setUser = useSetUser() const setUser = useSetUser()
useAccountRedirect() useAccountRedirect()
const setUser = useSetUser()
const redirectAfterLogin = user => { const redirectAfterLogin = user => {
setUser(user) setUser(user)
if (user.is_admin) if (user.is_admin)
Router.push('/admin') Router.push('/admin')
else else

Loading…
Cancel
Save