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
})
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')

@ -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;

@ -21,7 +21,7 @@ const Footer = () => {
<p>© 2015 - {DateTime.utc().year} Society of Socks</p>
</div>
<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="/privacy"><a>Privacy</a></Link></li>
</ul>

@ -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;
}
}

@ -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;
}
}

@ -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: [

@ -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',

@ -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));

@ -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

@ -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"

@ -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

@ -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 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}) {
<h3>Email and Password</h3>
<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]"? */}
<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>
<h3>Your Orders</h3>

@ -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

Loading…
Cancel
Save