From 95d7360ee4feae53b4fe66753f35e8eec03b86a6 Mon Sep 17 00:00:00 2001 From: benjaminramey Date: Wed, 18 Nov 2020 21:02:16 -0600 Subject: [PATCH] Use AJV, refactor a lot --- api/app.js | 11 ++++--- api/controllers/users.js | 32 +++++-------------- api/models/schemas.js | 10 ++++++ api/models/{users.js => users/index.js} | 42 ++++++++----------------- api/models/users/schemas.js | 19 +++++++++++ api/models/validation.js | 16 ++++++++++ api/package.json | 1 + api/utils/index.js | 29 +++++++++++++---- 8 files changed, 96 insertions(+), 64 deletions(-) create mode 100644 api/models/schemas.js rename api/models/{users.js => users/index.js} (65%) create mode 100644 api/models/users/schemas.js create mode 100644 api/models/validation.js diff --git a/api/app.js b/api/app.js index f6fea9c..58cd67c 100644 --- a/api/app.js +++ b/api/app.js @@ -76,10 +76,13 @@ app.get(`/*`, (req, res) => { /** * Error Handler */ -app.use(function (err, req, res) { - console.error(err); - res.status(500).json({ - error: `Internal Serverless Error - "${err.message}"`, +app.use(function (err, req, res, next) { + if (res.headersSent) { + return next(err); + } + res.status(err.code || 500); + res.json({ + error: err.message || `Internal Server Error - "${err.message}"`, }); }); diff --git a/api/controllers/users.js b/api/controllers/users.js index 8a71b98..9d26b98 100644 --- a/api/controllers/users.js +++ b/api/controllers/users.js @@ -3,29 +3,18 @@ */ const jwt = require(`jsonwebtoken`); -const { users } = require(`../models`); +const users = require(`../models/users`); const { comparePassword } = require(`../utils`); /** * Save * @param {*} req * @param {*} res - * @param {*} next */ -const register = async (req, res, next) => { - try { - await users.register(req.body); - } catch (error) { - return res.status(400).json({ error: error.message }); - } - - let user; - try { - user = await users.getByEmail(req.body.email); - } catch (error) { - console.log(error); - return next(error, null); - } +const register = async (req, res) => { + await users.create(req.body); + + let user = await users.getByEmail(req.body.email); const token = jwt.sign( users.convertToPublicFormat(user), @@ -45,15 +34,9 @@ const register = async (req, res, next) => { * Sign a user in * @param {*} req * @param {*} res - * @param {*} next */ -const login = async (req, res, next) => { - let user; - try { - user = await users.getByEmail(req.body.email); - } catch (error) { - return next(error, null); - } +const login = async (req, res) => { + let user = await users.getByEmail(req.body.email); if (!user) { return res @@ -86,7 +69,6 @@ const login = async (req, res, next) => { * Get a user * @param {*} req * @param {*} res - * @param {*} next */ const get = async (req, res) => { const user = users.convertToPublicFormat(req.user); diff --git a/api/models/schemas.js b/api/models/schemas.js new file mode 100644 index 0000000..a4230cc --- /dev/null +++ b/api/models/schemas.js @@ -0,0 +1,10 @@ +module.exports = { + "email": { + "type": "string", + "format": "email" + }, + "id": { + "type": "string", + "minLength": 1 + } +} diff --git a/api/models/users.js b/api/models/users/index.js similarity index 65% rename from api/models/users.js rename to api/models/users/index.js index 710a828..5afe712 100644 --- a/api/models/users.js +++ b/api/models/users/index.js @@ -2,30 +2,23 @@ /** * Model: Users */ - -const utils = require(`../utils`); -const db = require(`./db`); +const utils = require(`../../utils`); +const db = require(`../db`); +const schemas = require(`./schemas`); +const sharedSchemas = require(`../schemas`); +const validation = require(`../validation`); /** - * Register user + * Create a new user * @param {string} user.email User email * @param {string} user.password User password */ -const register = async (user = {}) => { - if (!user.email) { - throw new Error(`"email" is required`); - } - if (!user.password) { - throw new Error(`"password" is required`); - } - if (!utils.validateEmailAddress(user.email)) { - throw new Error(`"${user.email}" is not a valid email address`); - } +const create = async (user = {}) => { + validation.validateOrThrow(schemas.create, user); - // Check if user is already registered const existingUser = await getByEmail(user.email); if (existingUser) { - throw new Error( + throw utils.customError( `A user with email "${user.email}" is already registered` ); } @@ -40,16 +33,10 @@ const register = async (user = {}) => { /** * Get user by email address - * @param {string} email + * @param {string} email Email address of user to retrieve */ - const getByEmail = async (email) => { - if (!email) { - throw new Error(`"email" is required`); - } - if (!utils.validateEmailAddress(email)) { - throw new Error(`"${email}" is not a valid email address`); - } + validation.validateOrThrow(sharedSchemas.email, email); let user = await db.getByKey(email); @@ -61,11 +48,8 @@ const getByEmail = async (email) => { * Get user by id * @param {string} id */ - const getById = async (id) => { - if (!id) { - throw new Error(`"id" is required`); - } + validation.validateOrThrow(sharedSchemas.id, id); let user = await db.getById(`user`, id); @@ -97,7 +81,7 @@ const convertToPublicFormat = (user = {}) => { }; module.exports = { - register, + create, getByEmail, getById, convertToPublicFormat, diff --git a/api/models/users/schemas.js b/api/models/users/schemas.js new file mode 100644 index 0000000..6a6c1fe --- /dev/null +++ b/api/models/users/schemas.js @@ -0,0 +1,19 @@ +const sharedSchemas = require(`../schemas`); + +module.exports = { + "create": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Create User", + "description": "An object to create a new user", + "type": "object", + "properties": { + "email": sharedSchemas.email, + "password": { + "type": "string", + "minLength": 10, + "maxLength": 50 + } + }, + "required": ["email", "password"] + } +}; diff --git a/api/models/validation.js b/api/models/validation.js new file mode 100644 index 0000000..1b6b15e --- /dev/null +++ b/api/models/validation.js @@ -0,0 +1,16 @@ +const Ajv = require(`ajv`); + +const { validationError } = require(`../utils`); + +const validateOrThrow = (schema, data) => { + const ajv = new Ajv(); + const valid = ajv.validate(schema, data); + + if (!valid) { + throw validationError(ajv.errors); + } +}; + +module.exports = { + validateOrThrow, +}; diff --git a/api/package.json b/api/package.json index 667ba4c..4c0153c 100644 --- a/api/package.json +++ b/api/package.json @@ -4,6 +4,7 @@ "description": "", "main": "app.js", "dependencies": { + "ajv": "^6.12.6", "aws-sdk": "^2.792.0", "bcryptjs": "^2.4.3", "express": "^4.17.1", diff --git a/api/utils/index.js b/api/utils/index.js index 0f36b9b..1870e71 100644 --- a/api/utils/index.js +++ b/api/utils/index.js @@ -5,16 +5,30 @@ const bcrypt = require(`bcryptjs`); /** - * Validate email address + * Build a custom error + * @param {string} message Error message + * @param {number} code Error code, should be HTTP status code */ -const validateEmailAddress = (email) => { - var re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - return re.test(String(email).toLowerCase()); +const customError = (message, code) => { + const error = new Error(message); + error.code = code; + return error; +}; + +/** + * Build a custom validation error + * @param {array[string]} validationErrors Validation errors + */ +const validationError = (validationErrors) => { + let error = new Error("Validation error(s)"); + error.validationErrors = validationErrors; + error.code = 400; + return error; }; /** * Hash password - * @param {*} user + * @param {string} password Password to hash */ const hashPassword = (password) => { const salt = bcrypt.genSaltSync(10); @@ -23,13 +37,16 @@ const hashPassword = (password) => { /** * Compare password + * @param {string} candidatePassword Hashed password supplied by user + * @param {string} trustedPassword Hashed password on record for user */ const comparePassword = (candidatePassword, trustedPassword) => { return bcrypt.compareSync(candidatePassword, trustedPassword); }; module.exports = { + customError, + validationError, hashPassword, comparePassword, - validateEmailAddress, };