Password reset implemetation

main
Ashelyn Dawn 3 years ago
parent 99d2a65e17
commit dded710211

@ -1,9 +1,7 @@
const router = module.exports = require('express-promise-router')()
const ensureUser = require('./middleware/ensureUser')
const db = require('../db')
const sendgrid = require('@sendgrid/mail')
sendgrid.setApiKey(process.env.SENDGRID_KEY)
const email = require('../utils/email')
router.get('/links', ensureUser, async (req, res) => {
const links = await db.user.getOpenEmailLinks(req.user.uuid)
@ -18,19 +16,8 @@ router.post('/', ensureUser, async (req, res) => {
msg: 'Email address already verified'
}]})
const confirmUrl = await db.user.createLoginLink(req.user.uuid)
const msg = {
to: req.user.email,
from: {email: 'registration@email.societyofsocks.us', name: 'Society of Socks'},
templateId: 'd-33407f1dd1b14b7b84dd779511039c95',
dynamic_template_data: {
confirmUrl: confirmUrl
}
};
await sendgrid.send(msg);
await email.sendAccountConfirmation(req.user)
res.json({sent: true})
})
@ -42,7 +29,7 @@ router.get('/confirm/:uuid', ensureUser, async (req, res) => {
if(!validLink)
return res.redirect('/account/email/invalid')
await db.user.markLoginLinkUsed(validLink.uuid)
await db.user.markLinkUsed(validLink.uuid)
await db.user.markEmailVerified(validLink.user_uuid)
return res.redirect('/account')

@ -3,6 +3,7 @@ const parseJSON = require('body-parser').json()
const db = require('../db')
const ensureAdmin = require('./middleware/ensureAdmin')
const ensureUser = require('./middleware/ensureUser')
const email = require('../utils/email')
const sendgrid = require('@sendgrid/mail')
sendgrid.setApiKey(process.env.SENDGRID_KEY)
@ -36,22 +37,39 @@ router.post('/', parseJSON, registerValidation, async (req, res) => {
}
await db.session.create(req, user)
await email.sendAccountConfirmation(user)
// Send login email TODO: Abstract this so api/email and this route use the same function
const confirmUrl = await db.user.createLoginLink(user.uuid)
res.json(user)
})
const msg = {
to: user.email,
from: {email: 'registration@email.societyofsocks.us', name: 'Society of Socks'},
templateId: 'd-33407f1dd1b14b7b84dd779511039c95',
dynamic_template_data: {
confirmUrl: confirmUrl
}
};
router.post('/recover', parseJSON, validate.validEmail('email'), async (req, res) => {
const user = await db.user.findByEmail(req.body.email)
await sendgrid.send(msg);
if(user)
email.sendPasswordReset(user)
res.json(user)
res.end()
})
router.post('/recover/password', parseJSON,
validate.validPassword('password'),
validate.bothPasswordsMatch, async (req, res) => {
const {password, link_uuid, link_key} = req.body
const user = await db.user.verifyPasswordReset(link_uuid, link_key)
if(!user){
return res.status(422).json({errors: [{
param: 'password',
msg: 'Invalid reset link'
}]})
}
const updatedUser = await db.user.overwritePassword(user.uuid, password)
await db.user.markLinkUsed(link_uuid)
await db.session.create(req, updatedUser)
res.json(updatedUser)
})
router.get('/', ensureAdmin, async (req, res) => {

@ -32,6 +32,7 @@ module.exports = [
mapId: 'emailLinkMap',
idProperty: 'uuid',
properties: [
'type',
'time_created',
'timeout_length',
'login_hash',

@ -87,7 +87,11 @@ user.changePassword = async (user_uuid, oldPassword, newPassword) => {
if(!passwordCorrect)
return null
const newHash = await bcrypt.hash(newPassword, saltRounds)
return await user.overwritePassword(user_uuid, newPassword)
}
user.overwritePassword = async (user_uuid, new_password) => {
const newHash = await bcrypt.hash(new_password, saltRounds)
const query = {
text: 'select * from sos.change_password($1, $2)',
@ -101,6 +105,7 @@ user.changePassword = async (user_uuid, oldPassword, newPassword) => {
const {rows} = await pg.query(query)
return joinjs.map(rows, mappings, 'userMap', 'user_')[0];
}
user.getOpenEmailLinks = (user_uuid) =>
@ -124,7 +129,8 @@ user.createLoginLink = async (user_uuid) => {
params: [
user_uuid,
'2 hours',
hash
hash,
'email_confirm'
],
returnType: 'emailLink',
tablePrefix: 'email_link_',
@ -137,7 +143,7 @@ user.createLoginLink = async (user_uuid) => {
user.verifyLoginLink = async (link_uuid, key) => {
const link_record = await dbUtil.executeQuery({
query: {
text: 'select * from sos.email_link where email_link_uuid = $1',
text: 'select * from sos.email_link where email_link_uuid = $1 and email_link_time_used is null',
values: [link_uuid]
},
returnType: 'emailLink',
@ -154,11 +160,58 @@ user.verifyLoginLink = async (link_uuid, key) => {
const valid = await bcrypt.compare(key, link_record.login_hash)
if(!valid) return null
if(link_record.type !== 'email_confirm') return null
return link_record
}
user.markLoginLinkUsed = link_uuid =>
user.createPasswordReset = async user_uuid => {
const linkCode = uuid.v4()
const hash = await bcrypt.hash(linkCode, saltRounds)
const link_record = await dbUtil.executeFunction({
name: 'create_login_link',
params: [
user_uuid,
'2 hours',
hash,
'password_reset'
],
returnType: 'emailLink',
tablePrefix: 'email_link_',
single: true
})
return `${process.env.EXTERNAL_URL}/account/reset-password?id=${link_record.uuid}&key=${linkCode}`
}
user.verifyPasswordReset = async (link_uuid, key) => {
const link_record = await dbUtil.executeQuery({
query: {
text: 'select * from sos.email_link where email_link_uuid = $1 and email_link_time_used is null',
values: [link_uuid]
},
returnType: 'emailLink',
tablePrefix: 'email_link_',
single: true
})
if(!link_record){
// Avoid early exit timing difference
await bcrypt.hash(key, saltRounds)
return null
}
const valid = await bcrypt.compare(key, link_record.login_hash)
if(!valid) return null
if(link_record.type !== 'password_reset') return null
return user.findById(link_record.user_uuid)
}
user.markLinkUsed = link_uuid =>
dbUtil.executeFunction({
name: 'set_link_used',
params: [link_uuid],

@ -3,6 +3,7 @@ create type sos."transaction_state_enum" as enum ('started', 'completed', 'canc
create type sos."payment_type_enum" as enum ('ks_reward', 'stripe', 'paypal', 'account_credit');
create type sos."stockchange_type_enum" as enum ('purchase', 'shipment', 'admin');
create type sos."stock_change_dir_enum" as enum ('added', 'subtracted');
create type sos."email_link_type_enum" as enum ('email_confirm', 'email_change', 'password_reset');
create table sos."user" (
user_uuid uuid primary key default uuid_generate_v4(),
@ -17,6 +18,7 @@ create table sos."user" (
create table sos."email_link" (
email_link_uuid uuid primary key default uuid_generate_v4(),
email_link_type sos.email_link_type_enum not null,
email_link_user_uuid uuid not null references sos."user" (user_uuid),
email_link_time_created timestamptz not null default now(),
email_link_timeout_length interval not null,

@ -1165,7 +1165,7 @@ begin
and email_link_time_created + email_link_timeout_length > now();
end; $function$;
create or replace function sos.create_login_link(_user_uuid uuid, _timeout_length interval, _link_hash text)
create or replace function sos.create_login_link(_user_uuid uuid, _timeout_length interval, _link_hash text, _type sos.email_link_type_enum)
returns setof sos."email_link"
language plpgsql
as $function$
@ -1175,11 +1175,13 @@ begin
insert into sos."email_link" (
email_link_user_uuid,
email_link_timeout_length,
email_link_login_hash
email_link_login_hash,
email_link_type
) values (
_user_uuid,
_timeout_length,
_link_hash
_link_hash,
_type
) returning email_link_uuid into _link_uuid;
return query select * from sos."email_link" where email_link_uuid = _link_uuid;

@ -41,7 +41,9 @@ export default function AccountPage({orders}) {
<h3>Email and Password</h3>
<div style={{maxWidth: 700, margin: '0 auto'}}>
<p><strong>Email:</strong> {user.email} <Link href="/account/change-email"><a>Change</a></Link></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> {

@ -0,0 +1,40 @@
import React, {useState} from 'react'
import Link from 'next/link'
import Router from 'next/router'
import Head from 'next/head'
import isEmail from 'validator/lib/isEmail'
import {FormController, Input, Button} from '~/components/form'
import useAccountRedirect from '~/hooks/useAccountRedirect'
export default function ResetPassword(){
useAccountRedirect()
const [submitted, setSubmitted] = useState(false)
return (
<>
<Head>
<title>Reset Password | Society of Socks</title>
</Head>
{submitted
? (
<FormController>
<h1>Reset Password</h1>
<p>
An email has been sent to the provided email address - check your
email for further instructions in resetting your password.
</p>
</FormController>
)
: (
<FormController url="/api/users/recover" afterSubmit={()=>setSubmitted(true)}>
<h1>Reset Password</h1>
<Input label="Email" type="text" name="email" validate={value=>isEmail(value)} hint="Enter a valid email address" />
<Button type="submit">Reset Password</Button>
</FormController>
)
}
</>
)
}

@ -0,0 +1,54 @@
import React from 'react'
import Link from 'next/link'
import Router from 'next/router'
import axios from 'axios'
import Head from 'next/head'
import {useSetUser} from '~/hooks/useUser'
import {FormController, Input, Button} from '~/components/form'
import useAccountRedirect from '~/hooks/useAccountRedirect'
ResetPassword.getInitialProps = async ({ctx}) => {
const {id, key} = ctx.query
return {
link_uuid: id,
link_key: key
}
}
export default function ResetPassword({link_uuid, link_key}){
useAccountRedirect()
const setUser = useSetUser()
async function submitReset({password, password2}) {
const {data: user} = await axios.post(`/api/users/recover/password`, {
password,
password2,
link_uuid,
link_key,
})
setUser(user)
if (user.is_admin)
Router.push('/admin')
else
Router.push('/account')
}
return (
<>
<Head>
<title>Reset Password | Society of Socks</title>
</Head>
<FormController afterSubmit={submitReset}>
<h1>Reset Password</h1>
<Input label="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">Reset Password</Button>
</FormController>
</>
)
}

@ -7,10 +7,8 @@ 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'
export default function Login(){
const setUser = useSetUser()
useAccountRedirect()
const setUser = useSetUser()
@ -33,6 +31,7 @@ export default function Login(){
<Input label="Email" type="text" name="email" validate={value=>isEmail(value)} hint="Enter a valid email address" />
<Input label="Password" type="password" name="password" validate={value=>(value.length >= 8)} hint="Password must be at least 8 characters long" />
<Button type="submit">Submit</Button>
<p>Forgot your password? <Link href="/account/recover"><a>Reset your password</a></Link>.</p>
<p>Need an account? <Link href="/register"><a>Register here</a></Link>.</p>
</FormController>
</>

@ -0,0 +1,35 @@
const db = require('../db')
const sendgrid = require('@sendgrid/mail')
sendgrid.setApiKey(process.env.SENDGRID_KEY)
const email = module.exports = {}
email.sendAccountConfirmation = async user => {
const confirmUrl = await db.user.createLoginLink(user.uuid)
const msg = {
to: user.email,
from: {email: 'registration@email.societyofsocks.us', name: 'Society of Socks'},
templateId: 'd-33407f1dd1b14b7b84dd779511039c95',
dynamic_template_data: {
confirmUrl: confirmUrl
}
};
await sendgrid.send(msg);
}
email.sendPasswordReset = async user => {
const resetURL = await db.user.createPasswordReset(user.uuid)
const msg = {
to: user.email,
from: {email: 'accounts@email.societyofsocks.us', name: 'Society of Socks'},
templateId: 'd-90d751cfc8cd4047be39c994ff9d0f5c',
dynamic_template_data: {
resetPasswordURL: resetURL
}
}
await sendgrid.send(msg)
}
Loading…
Cancel
Save