minapi
Version:
Minimum viable API w/ authentication and permissions, CRUD and resource management
479 lines (400 loc) • 12.6 kB
JavaScript
const _ = require('underscore')
module.exports = (API, { config }) => {
const { _verify } = config
API.Auth = { ...require('./helpers')({ config }) }
//middleware requiring jwt tokens (verify, auth, and reset jwt tokens)
API.Auth.requireToken = async (req, res, next) => {
const { authorization } = req.headers
let { token } = req.body
try {
//only if we even have authorization headers (which we may not for reset and verifications)
if (authorization) {
const authToken = authorization.split('Bearer ')[1]
if (!token && !authToken) {
throw { code: 422, err: `missing token or malformed headers!` }
} else if (!token && authToken) {
token = authToken
}
}
//checking token validity
const decoded = await API.Utils.try('Auth.requireToken',
API.Auth.validateToken(token))
if (!decoded) { throw { code: 422, err: `malformed, expired, or invalid token!` } }
//persisting decoded token if whitelisted
switch (decoded.sub) {
case 'verify':
case 'auth':
case 'reset':
req[decoded.sub] = decoded
break
default:
throw { code: 422, err: `invalid token subject!` }
}
next()
}
catch (err) {
API.Utils.errorHandler({ res, err })
}
}
//middleware loading user from db
API.Auth.requireUser = async (req, res, next) => {
if (!req.auth) {
throw { code: 422, err: `invalid authentication provided!` }
}
let { _id } = req.auth
try {
req.user = await API.DB.user.read({ where: { _id } })
if (!req.user) { throw { code: 422, err: `user could not be found with credentials!` }}
next()
}
catch (err) {
API.Utils.errorHandler({ res, err })
}
}
//middleware requiring verified user
API.Auth.requireVerifiedUser = async (req, res, next) => {
const { user } = req
try {
if (user.email_verified !== true && user.sms_verified !== true) {
throw { code: 422, err: `user not yet verified!` }
}
next()
}
catch (err) {
API.Utils.errorHandler({ res, err })
}
}
//registration route
API.post('/register', [], async (req, res) => {
const { email, password } = req.body
try {
//checking if user email already registered
let user = await API.DB.user.read({ where: { email } })
if (user) {
throw { code: 400, err: `${email} is already registered!` }
}
//normalize sms to acceptable format
// const validSMS = API.Auth.normalizePhone(sms)
//checking if user sms already registered
// user = await API.DB.user.read({ where: { sms: validSMS.normalized } })
// if (user) { throw `${validSMS.normalized} is already registered!` }
//creating user
user = await API.DB.user.create({
values: {
..._.omit(req.body, ['sms', 'password']),
password_hash: await API.Auth.hashPassword(password),
email_verified: false,
}
})
res.status(200).send({
message: `user registered!`,
..._.omit(user, ['password_hash', 'password'])
})
}
catch (err) {
API.Utils.errorHandler({ res, err })
}
})
//login route
API.post('/login', [], async (req, res) => {
const { email, password } = req.body
try {
const user = await API.DB.user.read({ where: { email } })
if (!user) { throw `user not found!` }
const correctPassword = await API.Utils.try('Auth.login:comparePasswordWithHashed',
API.Auth.comparePasswordWithHashed(password, user.password_hash))
if (!correctPassword) { throw { code: 422, err: `incorrect login information!` } }
const token = await API.Utils.try('Auth.login:createToken',
API.Auth.createToken('auth', user._id, {}))
console.log({ token })
res.status(200).send({ token, message: `logged in!` })
}
catch (err) {
API.Utils.errorHandler({ res, err })
}
})
//user information
API.get('/user', [API.Auth.requireToken, API.Auth.requireUser], async (req, res) => {
try {
res.status(200).send(
_.omit(req.user, ['password_hash', '_id', 'created_at'])
)
}
catch (err) {
API.Utils.errorHandler({ res, err })
}
})
API.Checks.register({
resource: '/register',
description: 'register new user',
method: 'POST',
params: ``,
bearerToken: ``,
body: `({ first_name: output.firstName, last_name: output.lastName, email: output.email, password: output.password })`,
output: `({ output, request }) => ({ ...output, email: request.body.email, password: request.body.password })`,
expectedStatusCode: 200,
})
API.Checks.register({
resource: '/login',
description: 'login user',
method: 'POST',
params: ``,
bearerToken: ``,
body: `({ email: output.email, password: output.password })`,
output: `({ data, output }) => ({ ...output, token: data.token })`,
expectedStatusCode: 200,
})
API.Checks.register({
resource: '/user',
description: 'get user information',
method: 'GET',
params: ``,
bearerToken: `(output.token)`,
body: ``,
output: `({ data, output }) => ({ ...output, user: data })`,
expectedStatusCode: 200,
})
//request reset password instructions be emailed
API.post('/user/reset/password', [], async (req, res) => {
const { email } = req.body
let user, token
try {
//checking if user exists via email
user = await API.DB.user.read({ where: { email } })
//creating jwt token
token = await API.Utils.try('Auth.resetPassword:createToken',
API.Auth.createToken('reset', user._id, {}))
res.status(200).send({
token,
message: `emailing reset password instructions!`
})
}
catch (err) {
API.Utils.errorHandler({ res, err })
}
try {
//send email for reset password (outside of main try catch)
const duration = API.Auth.expirations('reset')
const url = process.env.APP_URL_RESET_PASSWORD.replace(':token', token)
const emailArgs = require('./emails/resetPassword')(email, {
url,
durationText: duration.text,
appName: process.env.APP_NAME,
appAuthor: process.env.APP_AUTHOR,
appAuthorEmail: process.env.APP_AUTHOR_EMAIL,
})
//sending email
await API.Utils.try('Auth.resetPassword:email.send',
API.Notifications.email.send(emailArgs))
}
catch (err) {
console.log('Auth.resetPassword', err)
}
})
API.Checks.register({
resource: '/user/reset/password',
description: 'emailing password reset instructions',
method: 'POST',
params: ``,
bearerToken: ``,
body: `({ email: output.email })`,
output: `({ data, output }) => ({ ...output, token: data.token })`,
expectedStatusCode: 200,
})
//change user's password
API.put('/user/reset/password', [API.Auth.requireToken], async(req, res) => {
const { _id } = req.reset
const { password } = req.body
try {
//resetting user's password
await API.DB.user.update({
where: { _id },
values: { password_hash: await API.Auth.hashPassword(password) },
})
res.status(200).send({ message: `changed password for user!` })
}
catch (err) {
API.Utils.errorHandler({ res, err })
}
try {
//getting user information
const user = await API.DB.user.read({ where: { _id } })
const emailArgs = require('./emails/changedPassword')(user.email, {
appName: process.env.APP_NAME,
appAuthor: process.env.APP_AUTHOR,
appAuthorEmail: process.env.APP_AUTHOR_EMAIL,
})
//sending email
API.Utils.try('Auth.resetPassword(put):email.send',
API.Notifications.email.send(emailArgs))
}
catch (err) {
console.log(err)
}
})
API.Checks.register({
resource: '/user/reset/password',
description: 'update password after clicking email link',
method: 'PUT',
params: ``,
bearerToken: `(output.token)`,
body: `({ password: output.newPassword })`,
output: `({ output, request }) => ({ ...output, password: request.body.password })`,
expectedStatusCode: 200,
})
API.Checks.register({
resource: '/login',
description: 'login user after changing password',
method: 'POST',
params: ``,
bearerToken: ``,
body: `({ email: output.email, password: output.newPassword })`,
output: `({ data, output }) => ({ ...output, token: data.token })`,
expectedStatusCode: 200,
})
API.Checks.register({
resource: '/user',
description: 'get user information after changing password',
method: 'GET',
params: ``,
bearerToken: `(output.token)`,
body: ``,
output: `({ data, output }) => ({ ...output, user: data })`,
expectedStatusCode: 200,
})
API.post('/user/verify/:method', [API.Auth.requireToken, API.Auth.requireUser], async(req, res) => {
const { email, sms, _id } = req.user
const { method } = req.params
let payload = { method, value: '' }
let token
try {
//ensuring appropriate verification value given method
if (method === 'email') {
payload.value = email
} else if (method === 'sms') {
payload.value = sms
}
//creating necessary token and link for verifying method
token = await API.Utils.try('Auth.verify:createToken',
API.Auth.createToken('verify', _id, payload))
res.status(200).send({ message: `sending verification!`, token })
}
catch (err) {
API.Utils.errorHandler({ res, err })
}
try {
const url = process.env.APP_URL_VERIFY.replace(':token', token)
//delivering email verification notification
if (method === 'email') {
const emailArgs = require('./emails/verifyEmail')(email, {
url,
appName: process.env.APP_NAME,
appAuthor: process.env.APP_AUTHOR,
})
//sending email
API.Utils.try('Auth.verify:email.send',
API.Notifications.email.send(emailArgs))
//delivering sms verification notification
} else if (method === 'sms') {
console.log({ method, token })
}
}
catch (err) {
console.log(err)
}
})
API.Checks.register({
resource: '/user/verify/:method',
description: 'verifying email',
method: 'POST',
params: `({ method: 'email' })`,
bearerToken: `(output.token)`,
body: ``,
output: `({ data, output, request }) => ({ ...output, token: data.token, method: request.params.method })`,
expectedStatusCode: 200,
})
API.get('/user/verify', [API.Auth.requireToken], async (req, res) => {
//verify token carries all necessary info (w/ verify method)
const { _id, method, value } = req.verify
try {
switch (method) {
case 'email':
case 'sms':
break
default:
throw `${method} verification unavailable!`
}
//we pass the value (email or sms) to ensure less likely for anonymous public attempts at gaining user status, as opposed to via url param (since only requiring jwt token and not user auth)
let where = { _id }
where[method] = value
where[`${method}_verified`] = true
//updating user verification status
const user = await API.DB.user.read({ where })
console.log('user/verify', where, { user })
res.status(200).send({
status: user ? true : false,
message: `user ${method} ${user ? 'is verified' : 'not verified'}!`
})
}
catch (err) {
API.Utils.errorHandler({ res, err })
}
})
API.Checks.register({
resource: '/user/verify',
description: 'checking email verification status',
method: 'GET',
params: ``,
bearerToken: `(output.token)`,
body: ``,
output: `({ output }) => ({ ...output })`,
expectedStatusCode: 200,
})
//complete verification of method
API.put('/user/verify', [API.Auth.requireToken], async (req, res) => {
//verify token carries all necessary info (w/ verify method)
const { _id, method, value } = req.verify
try {
switch (method) {
case 'email':
case 'sms':
break
default:
throw `${method} verification unavailable!`
}
let where, values
//we pass the value (email or sms) to ensure less likely for anonymous public attempts at gaining user status, as opposed to via url param (since only requiring jwt token and not user auth)
where = { _id }
where[method] = value
values = {}
values[`${method}_verified`] = true
//updating user verification status
await API.DB.user.update({ where, values })
res.status(200).send({ message: `user ${method} verified!` })
}
catch (err) {
API.Utils.errorHandler({ res, err })
}
})
API.Checks.register({
resource: '/user/verify',
description: 'completing email verification',
method: 'PUT',
params: ``,
bearerToken: `(output.token)`,
body: ``,
output: `({ output }) => ({ ...output })`,
expectedStatusCode: 200,
})
API.Checks.register({
resource: '/user/verify',
description: 'checking email verification status',
method: 'GET',
params: ``,
bearerToken: `(output.token)`,
body: ``,
output: `({ output }) => ({ ...output })`,
expectedStatusCode: 200,
})
return API
}