Password reset implemetation

main
Ashelyn Dawn 4 years ago
parent 99d2a65e17
commit dded710211

@ -1,9 +1,7 @@
const router = module.exports = require('express-promise-router')() const router = module.exports = require('express-promise-router')()
const ensureUser = require('./middleware/ensureUser') const ensureUser = require('./middleware/ensureUser')
const db = require('../db') const db = require('../db')
const email = require('../utils/email')
const sendgrid = require('@sendgrid/mail')
sendgrid.setApiKey(process.env.SENDGRID_KEY)
router.get('/links', ensureUser, async (req, res) => { router.get('/links', ensureUser, async (req, res) => {
const links = await db.user.getOpenEmailLinks(req.user.uuid) const links = await db.user.getOpenEmailLinks(req.user.uuid)
@ -18,18 +16,7 @@ router.post('/', ensureUser, async (req, res) => {
msg: 'Email address already verified' msg: 'Email address already verified'
}]}) }]})
const confirmUrl = await db.user.createLoginLink(req.user.uuid) await email.sendAccountConfirmation(req.user)
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);
res.json({sent: true}) res.json({sent: true})
}) })
@ -42,7 +29,7 @@ router.get('/confirm/:uuid', ensureUser, async (req, res) => {
if(!validLink) if(!validLink)
return res.redirect('/account/email/invalid') 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) await db.user.markEmailVerified(validLink.user_uuid)
return res.redirect('/account') return res.redirect('/account')

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

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

@ -87,7 +87,11 @@ user.changePassword = async (user_uuid, oldPassword, newPassword) => {
if(!passwordCorrect) if(!passwordCorrect)
return null 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 = { const query = {
text: 'select * from sos.change_password($1, $2)', 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) const {rows} = await pg.query(query)
return joinjs.map(rows, mappings, 'userMap', 'user_')[0]; return joinjs.map(rows, mappings, 'userMap', 'user_')[0];
} }
user.getOpenEmailLinks = (user_uuid) => user.getOpenEmailLinks = (user_uuid) =>
@ -124,7 +129,8 @@ user.createLoginLink = async (user_uuid) => {
params: [ params: [
user_uuid, user_uuid,
'2 hours', '2 hours',
hash hash,
'email_confirm'
], ],
returnType: 'emailLink', returnType: 'emailLink',
tablePrefix: 'email_link_', tablePrefix: 'email_link_',
@ -137,7 +143,7 @@ user.createLoginLink = async (user_uuid) => {
user.verifyLoginLink = async (link_uuid, key) => { user.verifyLoginLink = async (link_uuid, key) => {
const link_record = await dbUtil.executeQuery({ const link_record = await dbUtil.executeQuery({
query: { 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] values: [link_uuid]
}, },
returnType: 'emailLink', returnType: 'emailLink',
@ -154,11 +160,58 @@ user.verifyLoginLink = async (link_uuid, key) => {
const valid = await bcrypt.compare(key, link_record.login_hash) const valid = await bcrypt.compare(key, link_record.login_hash)
if(!valid) return null if(!valid) return null
if(link_record.type !== 'email_confirm') return null
return link_record 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({ dbUtil.executeFunction({
name: 'set_link_used', name: 'set_link_used',
params: [link_uuid], 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."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."stockchange_type_enum" as enum ('purchase', 'shipment', 'admin');
create type sos."stock_change_dir_enum" as enum ('added', 'subtracted'); 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" ( create table sos."user" (
user_uuid uuid primary key default uuid_generate_v4(), user_uuid uuid primary key default uuid_generate_v4(),
@ -17,6 +18,7 @@ create table sos."user" (
create table sos."email_link" ( create table sos."email_link" (
email_link_uuid uuid primary key default uuid_generate_v4(), 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_user_uuid uuid not null references sos."user" (user_uuid),
email_link_time_created timestamptz not null default now(), email_link_time_created timestamptz not null default now(),
email_link_timeout_length interval not null, email_link_timeout_length interval not null,

@ -1165,7 +1165,7 @@ begin
and email_link_time_created + email_link_timeout_length > now(); and email_link_time_created + email_link_timeout_length > now();
end; $function$; 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" returns setof sos."email_link"
language plpgsql language plpgsql
as $function$ as $function$
@ -1175,11 +1175,13 @@ begin
insert into sos."email_link" ( insert into sos."email_link" (
email_link_user_uuid, email_link_user_uuid,
email_link_timeout_length, email_link_timeout_length,
email_link_login_hash email_link_login_hash,
email_link_type
) values ( ) values (
_user_uuid, _user_uuid,
_timeout_length, _timeout_length,
_link_hash _link_hash,
_type
) returning email_link_uuid into _link_uuid; ) returning email_link_uuid into _link_uuid;
return query select * from sos."email_link" where email_link_uuid = _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> <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} <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]"? */} {/* TODO: Store date password was set so we can show "Set on [date]"? */}
<p><strong>Password:</strong> { <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 {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'
export default function Login(){ export default function Login(){
const setUser = useSetUser()
useAccountRedirect() useAccountRedirect()
const setUser = useSetUser() 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="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" /> <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> <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> <p>Need an account? <Link href="/register"><a>Register here</a></Link>.</p>
</FormController> </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