SectionList on home screen with API

This commit is contained in:
John Lyon-Smith
2018-04-08 18:33:21 -07:00
parent 5634acb967
commit 7891bb71c9
19 changed files with 1278 additions and 1201 deletions

View File

@@ -1,13 +1,14 @@
import passport from 'passport'
import credential from 'credential'
import createError from 'http-errors'
import config from 'config'
import crypto from 'crypto'
import urlSafeBase64 from 'urlsafe-base64'
import util from 'util'
import * as loginToken from './loginToken'
import autobind from 'autobind-decorator'
import url from 'url'
import passport from "passport"
import credential from "credential"
import createError from "http-errors"
import config from "config"
import crypto from "crypto"
import urlSafeBase64 from "urlsafe-base64"
import util from "util"
import * as loginToken from "./loginToken"
import autobind from "autobind-decorator"
import url from "url"
import { catchAll } from "."
@autobind
export class AuthRoutes {
@@ -16,44 +17,60 @@ export class AuthRoutes {
this.mq = container.mq
this.db = container.db
this.maxEmailTokenAgeInHours = config.get('email.maxEmailTokenAgeInHours')
this.maxPasswordTokenAgeInHours = config.get('email.maxPasswordTokenAgeInHours')
this.sendEmailDelayInSeconds = config.get('email.sendEmailDelayInSeconds')
this.supportEmail = config.get('email.supportEmail')
this.sendEmail = config.get('email.sendEmail')
app.route('/auth/login')
this.maxEmailTokenAgeInHours = config.get("email.maxEmailTokenAgeInHours")
this.maxPasswordTokenAgeInHours = config.get(
"email.maxPasswordTokenAgeInHours"
)
this.sendEmailDelayInSeconds = config.get("email.sendEmailDelayInSeconds")
this.supportEmail = config.get("email.supportEmail")
this.sendEmail = config.get("email.sendEmail")
app
.route("/auth/login")
// Used to login. Email must be confirmed.
.post(this.login)
.post(catchAll(this.login))
// Used to logout
.delete(passport.authenticate('bearer', { session: false }), this.logout)
.delete(
passport.authenticate("bearer", { session: false }),
catchAll(this.logout)
)
// Send change email confirmation email
app.route('/auth/email/send')
.post(passport.authenticate('bearer', { session: false }), this.sendChangeEmailEmail)
app
.route("/auth/email/send")
.post(
passport.authenticate("bearer", { session: false }),
catchAll(this.sendChangeEmailEmail)
)
// Confirm email address
app.route('/auth/email/confirm')
.post(this.confirmEmail)
app.route("/auth/email/confirm").post(catchAll(this.confirmEmail))
// Change the logged in users password, leaving user still logged in
app.route('/auth/password/change')
.post(passport.authenticate('bearer', { session: false }), this.changePassword)
app
.route("/auth/password/change")
.post(
passport.authenticate("bearer", { session: false }),
catchAll(this.changePassword)
)
// Send a password reset email
app.route('/auth/password/send')
.post(this.sendPasswordResetEmail)
app.route("/auth/password/send").post(catchAll(this.sendPasswordResetEmail))
// Confirm a password reset token is valid
app.route('/auth/password/confirm')
.post(this.confirmPasswordToken)
app
.route("/auth/password/confirm")
.post(catchAll(this.confirmPasswordToken))
// Finish a password reset
app.route('/auth/password/reset')
.post(this.resetPassword)
app.route("/auth/password/reset").post(catchAll(this.resetPassword))
// Indicate who the currently logged in user is
app.route('/auth/who')
.get(passport.authenticate('bearer', { session: false }), this.whoAmI)
app
.route("/auth/who")
.get(
passport.authenticate("bearer", { session: false }),
catchAll(this.whoAmI)
)
}
async login(req, res, next) {
@@ -61,62 +78,53 @@ export class AuthRoutes {
const password = req.body.password
let User = this.db.User
try {
if (!email || !password) {
createError.BadRequest('Must supply user name and password')
}
if (!email || !password) {
createError.BadRequest("Must supply user name and password")
}
// Lookup the user
const user = await User.findOne({ email })
// Lookup the user
const user = await User.findOne({ email })
if (!user) {
// NOTE: Don't return NotFound as that gives too much information away to hackers
throw createError.BadRequest("Email or password incorrect")
} else if (user.emailToken || !user.passwordHash) {
throw createError.Forbidden("Must confirm email and set password")
}
if (!user) {
// NOTE: Don't return NotFound as that gives too much information away to hackers
throw createError.BadRequest("Email or password incorrect")
} else if (user.emailToken || !user.passwordHash) {
throw createError.Forbidden("Must confirm email and set password")
}
let cr = credential()
const isValid = await cr.verify(JSON.stringify(user.passwordHash), req.body.password)
let cr = credential()
const isValid = await cr.verify(
JSON.stringify(user.passwordHash),
req.body.password
)
if (isValid) {
user.loginToken = loginToken.pack(user._id.toString(), user.email)
} else {
user.loginToken = null // A bad login removes existing token for this user...
}
if (isValid) {
user.loginToken = loginToken.pack(user._id.toString(), user.email)
} else {
user.loginToken = null // A bad login removes existing token for this user...
}
const savedUser = await user.save()
const savedUser = await user.save()
if (savedUser.loginToken) {
res.set('Authorization', `Bearer ${savedUser.loginToken}`)
res.json(savedUser.toClient())
} else {
throw createError.BadRequest('email or password incorrect')
}
} catch(err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
next(createError.InternalServerError(err.message))
}
if (savedUser.loginToken) {
res.set("Authorization", `Bearer ${savedUser.loginToken}`)
res.json(savedUser.toClient())
} else {
throw createError.BadRequest("email or password incorrect")
}
}
async logout(req, res, next) {
let User = this.db.User
try {
const user = await User.findById({ _id: req.user._id })
const user = await User.findById({ _id: req.user._id })
if (user) {
user.loginToken = null
await user.save()
}
res.json({})
} catch(err) {
next(createError.InternalServerError(err.message))
if (user) {
user.loginToken = null
await user.save()
}
res.json({})
}
whoAmI(req, res, next) {
@@ -129,175 +137,163 @@ export class AuthRoutes {
let User = this.db.User
const isAdmin = !!req.user.administrator
try {
if (existingEmail) {
if (!isAdmin) {
throw createError.Forbidden('Only admins can resend change email to any user')
}
} else {
existingEmail = req.user.email
if (existingEmail) {
if (!isAdmin) {
throw createError.Forbidden(
"Only admins can resend change email to any user"
)
}
} else {
existingEmail = req.user.email
}
const user = await User.findOne({ email: existingEmail })
let conflictingUser = null
const user = await User.findOne({ email: existingEmail })
let conflictingUser = null
if (newEmail) {
conflictingUser = await User.findOne({ email: newEmail })
}
if (newEmail) {
conflictingUser = await User.findOne({ email: newEmail })
}
if (!user) {
throw createError.NotFound(`User with email '${existingEmail}' was not found`)
} else if (conflictingUser) {
throw createError.BadRequest(`A user with '${newEmail}' already exists`)
} else if (!isAdmin && user.emailToken && (new Date() - user.emailToken.created) < this.sendEmailDelayInSeconds) {
throw createError.BadRequest('Cannot request email confirmation again so soon')
}
if (!user) {
throw createError.NotFound(
`User with email '${existingEmail}' was not found`
)
} else if (conflictingUser) {
throw createError.BadRequest(`A user with '${newEmail}' already exists`)
} else if (
!isAdmin &&
user.emailToken &&
new Date() - user.emailToken.created < this.sendEmailDelayInSeconds
) {
throw createError.BadRequest(
"Cannot request email confirmation again so soon"
)
}
const buf = await util.promisify(crypto.randomBytes)(32)
const buf = await util.promisify(crypto.randomBytes)(32)
user.emailToken = {
value: urlSafeBase64.encode(buf),
created: new Date()
}
user.emailToken = {
value: urlSafeBase64.encode(buf),
created: new Date(),
}
if (newEmail) {
user.newEmail = newEmail
}
if (newEmail) {
user.newEmail = newEmail
}
const savedUser = await user.save()
const userFullName = `${savedUser.firstName} ${savedUser.lastName}`
const siteUrl = url.parse(req.headers.referer)
let msgs = []
if (savedUser.newEmail) {
msgs.push({
toEmail: savedUser.email,
templateName: 'changeEmailOld',
templateData: {
recipientFullName: userFullName,
recipientNewEmail: savedUser.newEmail,
supportEmail: this.supportEmail
}
})
}
const savedUser = await user.save()
const userFullName = `${savedUser.firstName} ${savedUser.lastName}`
const siteUrl = url.parse(req.headers.referer)
let msgs = []
if (savedUser.newEmail) {
msgs.push({
toEmail: savedUser.newEmail || savedUser.email,
templateName: 'changeEmailNew',
toEmail: savedUser.email,
templateName: "changeEmailOld",
templateData: {
recipientFullName: userFullName,
confirmEmailLink: `${siteUrl.protocol}//${siteUrl.host}/confirm-email?email-token%3D${savedUser.emailToken.value}`,
supportEmail: this.supportEmail
}
recipientNewEmail: savedUser.newEmail,
supportEmail: this.supportEmail,
},
})
if (this.sendEmail) {
await this.mq.request('dar-email', 'sendEmail', msgs)
}
res.json({})
} catch(err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
next(createError.InternalServerError(err.message))
}
}
msgs.push({
toEmail: savedUser.newEmail || savedUser.email,
templateName: "changeEmailNew",
templateData: {
recipientFullName: userFullName,
confirmEmailLink: `${siteUrl.protocol}//${
siteUrl.host
}/confirm-email?email-token%3D${savedUser.emailToken.value}`,
supportEmail: this.supportEmail,
},
})
if (this.sendEmail) {
await this.mq.request("dar-email", "sendEmail", msgs)
}
res.json({})
}
async confirmEmail(req, res, next) {
const token = req.body.emailToken
let User = this.db.User
try {
if (!token) {
throw createError.BadRequest('Invalid request parameters')
}
if (!token) {
throw createError.BadRequest("Invalid request parameters")
}
const user = await User.findOne({ 'emailToken.value': token })
const user = await User.findOne({ "emailToken.value": token })
if (!user) {
throw createError.BadRequest(`The token was not found`)
}
if (!user) {
throw createError.BadRequest(`The token was not found`)
}
// Token must not be too old
const ageInHours = (new Date() - user.emailToken.created) / 3600000
// Token must not be too old
const ageInHours = (new Date() - user.emailToken.created) / 3600000
if (ageInHours > this.maxEmailTokenAgeInHours) {
throw createError.BadRequest(`Token has expired`)
}
if (ageInHours > this.maxEmailTokenAgeInHours) {
throw createError.BadRequest(`Token has expired`)
}
// Remove the email token & any login token as it will become invalid
user.emailToken = undefined
user.loginToken = undefined
// Remove the email token & any login token as it will become invalid
user.emailToken = undefined
user.loginToken = undefined
// Switch in any new email now
if (user.newEmail) {
user.email = user.newEmail
user.newEmail = undefined
}
// Switch in any new email now
if (user.newEmail) {
user.email = user.newEmail
user.newEmail = undefined
}
let buf = null
let buf = null
// If user has no password, create reset token for them
if (!user.passwordHash) {
buf = await util.promisify(crypto.randomBytes)(32)
// If user has no password, create reset token for them
if (!user.passwordHash) {
buf = await util.promisify(crypto.randomBytes)(32)
user.passwordToken = {
value: urlSafeBase64.encode(buf),
created: new Date()
}
}
const savedUser = await user.save()
let obj = {}
// Only because the user has sent us a valid email reset token
// can we respond with an password reset token.
if (savedUser.passwordToken) {
obj.passwordToken = savedUser.passwordToken.value
}
res.json(obj)
} catch(err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
next(createError.InternalServerError(err.message))
user.passwordToken = {
value: urlSafeBase64.encode(buf),
created: new Date(),
}
}
const savedUser = await user.save()
let obj = {}
// Only because the user has sent us a valid email reset token
// can we respond with an password reset token.
if (savedUser.passwordToken) {
obj.passwordToken = savedUser.passwordToken.value
}
res.json(obj)
}
async confirmPasswordToken(req, res, next) {
const token = req.body.passwordToken
let User = this.db.User
try {
if (!token) {
throw createError.BadRequest('Invalid request parameters')
}
const user = await User.findOne({ 'passwordToken.value': token })
if (!user) {
throw createError.BadRequest(`The token was not found`)
}
// Token must not be too old
const ageInHours = (new Date() - user.passwordToken.created) / (3600 * 1000)
if (ageInHours > this.maxPasswordTokenAgeInHours) {
throw createError.BadRequest(`Token has expired`)
}
res.json({})
} catch (err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
next(createError.InternalServerError(err.message))
}
if (!token) {
throw createError.BadRequest("Invalid request parameters")
}
const user = await User.findOne({ "passwordToken.value": token })
if (!user) {
throw createError.BadRequest(`The token was not found`)
}
// Token must not be too old
const ageInHours = (new Date() - user.passwordToken.created) / (3600 * 1000)
if (ageInHours > this.maxPasswordTokenAgeInHours) {
throw createError.BadRequest(`Token has expired`)
}
res.json({})
}
async resetPassword(req, res, next) {
@@ -306,118 +302,102 @@ export class AuthRoutes {
let User = this.db.User
let cr = credential()
try {
if (!token || !newPassword) {
throw createError.BadRequest('Invalid request parameters')
}
const user = await User.findOne({ 'passwordToken.value': token })
if (!user) {
throw createError.BadRequest(`The token was not found`)
}
// Token must not be too old
const ageInHours = (new Date() - user.passwordToken.created) / (3600 * 1000)
if (ageInHours > this.maxPasswordTokenAgeInHours) {
throw createError.BadRequest(`Token has expired`)
}
// Remove the password token & any login token
user.passwordToken = undefined
user.loginToken = undefined
const json = await cr.hash(newPassword)
user.passwordHash = JSON.parse(json)
await user.save()
res.json({})
} catch(err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
next(createError.InternalServerError(err.message))
}
if (!token || !newPassword) {
throw createError.BadRequest("Invalid request parameters")
}
const user = await User.findOne({ "passwordToken.value": token })
if (!user) {
throw createError.BadRequest(`The token was not found`)
}
// Token must not be too old
const ageInHours = (new Date() - user.passwordToken.created) / (3600 * 1000)
if (ageInHours > this.maxPasswordTokenAgeInHours) {
throw createError.BadRequest(`Token has expired`)
}
// Remove the password token & any login token
user.passwordToken = undefined
user.loginToken = undefined
const json = await cr.hash(newPassword)
user.passwordHash = JSON.parse(json)
await user.save()
res.json({})
}
async changePassword(req, res, next) {
let User = this.db.User
let cr = credential()
try {
const user = await User.findById({ _id: req.user._id })
const user = await User.findById({ _id: req.user._id })
if (!user) {
throw createError.NotFound(`User ${req.user._id} not found`)
}
const ok = await cr.verify(JSON.stringify(user.passwordHash), req.body.oldPassword)
const obj = await cr.hash(req.body.newPassword)
user.passwordHash = JSON.parse(obj)
await user.save()
res.json({})
} catch(err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
next(createError.InternalServerError(err.message))
}
if (!user) {
throw createError.NotFound(`User ${req.user._id} not found`)
}
const ok = await cr.verify(
JSON.stringify(user.passwordHash),
req.body.oldPassword
)
const obj = await cr.hash(req.body.newPassword)
user.passwordHash = JSON.parse(obj)
await user.save()
res.json({})
}
async sendPasswordResetEmail(req, res, next){
async sendPasswordResetEmail(req, res, next) {
const email = req.body.email
let User = this.db.User
try {
if (!email) {
throw createError.BadRequest('Invalid request parameters')
}
const user = await User.findOne({ email })
// User must exist and their email must be confirmed
if (!user || user.emailToken) {
// Don't give away any information about why we rejected the request
throw createError.BadRequest('Not a valid request')
} else if (user.passwordToken && user.passwordToken.created &&
(new Date() - user.passwordToken.created) < this.sendEmailDelayInSeconds) {
throw createError.BadRequest('Cannot request password reset so soon')
}
const buf = await util.promisify(crypto.randomBytes)(32)
user.passwordToken = {
value: urlSafeBase64.encode(buf),
created: new Date()
}
const savedUser = await user.save()
const userFullName = `${savedUser.firstName} ${savedUser.lastName}`
const siteUrl = url.parse(req.headers.referer)
const msg = {
toEmail: savedUser.email,
templateName: 'forgotPassword',
templateData: {
recipientFullName: userFullName,
resetPasswordLink: `${siteUrl.protocol}//${siteUrl.host}/reset-password?password-token%3D${savedUser.passwordToken.value}`,
supportEmail: this.supportEmail
}
}
if (this.sendEmail) {
await this.mq.request('dar-email', 'sendEmail', msg)
}
res.json({})
} catch(err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
next(createError.InternalServerError(`Unable to send password reset email. ${err.message}`))
}
if (!email) {
throw createError.BadRequest("Invalid request parameters")
}
const user = await User.findOne({ email })
// User must exist and their email must be confirmed
if (!user || user.emailToken) {
// Don't give away any information about why we rejected the request
throw createError.BadRequest("Not a valid request")
} else if (
user.passwordToken &&
user.passwordToken.created &&
new Date() - user.passwordToken.created < this.sendEmailDelayInSeconds
) {
throw createError.BadRequest("Cannot request password reset so soon")
}
const buf = await util.promisify(crypto.randomBytes)(32)
user.passwordToken = {
value: urlSafeBase64.encode(buf),
created: new Date(),
}
const savedUser = await user.save()
const userFullName = `${savedUser.firstName} ${savedUser.lastName}`
const siteUrl = url.parse(req.headers.referer)
const msg = {
toEmail: savedUser.email,
templateName: "forgotPassword",
templateData: {
recipientFullName: userFullName,
resetPasswordLink: `${siteUrl.protocol}//${
siteUrl.host
}/reset-password?password-token%3D${savedUser.passwordToken.value}`,
supportEmail: this.supportEmail,
},
}
if (this.sendEmail) {
await this.mq.request("dar-email", "sendEmail", msg)
}
res.json({})
}
}