Use AJV, refactor a lot

This commit is contained in:
2020-11-18 21:02:16 -06:00
parent ddf1a833d6
commit 95d7360ee4
8 changed files with 96 additions and 64 deletions

View File

@@ -76,10 +76,13 @@ app.get(`/*`, (req, res) => {
/** /**
* Error Handler * Error Handler
*/ */
app.use(function (err, req, res) { app.use(function (err, req, res, next) {
console.error(err); if (res.headersSent) {
res.status(500).json({ return next(err);
error: `Internal Serverless Error - "${err.message}"`, }
res.status(err.code || 500);
res.json({
error: err.message || `Internal Server Error - "${err.message}"`,
}); });
}); });

View File

@@ -3,29 +3,18 @@
*/ */
const jwt = require(`jsonwebtoken`); const jwt = require(`jsonwebtoken`);
const { users } = require(`../models`); const users = require(`../models/users`);
const { comparePassword } = require(`../utils`); const { comparePassword } = require(`../utils`);
/** /**
* Save * Save
* @param {*} req * @param {*} req
* @param {*} res * @param {*} res
* @param {*} next
*/ */
const register = async (req, res, next) => { const register = async (req, res) => {
try { await users.create(req.body);
await users.register(req.body);
} catch (error) { let user = await users.getByEmail(req.body.email);
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 token = jwt.sign( const token = jwt.sign(
users.convertToPublicFormat(user), users.convertToPublicFormat(user),
@@ -45,15 +34,9 @@ const register = async (req, res, next) => {
* Sign a user in * Sign a user in
* @param {*} req * @param {*} req
* @param {*} res * @param {*} res
* @param {*} next
*/ */
const login = async (req, res, next) => { const login = async (req, res) => {
let user; let user = await users.getByEmail(req.body.email);
try {
user = await users.getByEmail(req.body.email);
} catch (error) {
return next(error, null);
}
if (!user) { if (!user) {
return res return res
@@ -86,7 +69,6 @@ const login = async (req, res, next) => {
* Get a user * Get a user
* @param {*} req * @param {*} req
* @param {*} res * @param {*} res
* @param {*} next
*/ */
const get = async (req, res) => { const get = async (req, res) => {
const user = users.convertToPublicFormat(req.user); const user = users.convertToPublicFormat(req.user);

10
api/models/schemas.js Normal file
View File

@@ -0,0 +1,10 @@
module.exports = {
"email": {
"type": "string",
"format": "email"
},
"id": {
"type": "string",
"minLength": 1
}
}

View File

@@ -2,30 +2,23 @@
/** /**
* Model: Users * Model: Users
*/ */
const utils = require(`../../utils`);
const utils = require(`../utils`); const db = require(`../db`);
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.email User email
* @param {string} user.password User password * @param {string} user.password User password
*/ */
const register = async (user = {}) => { const create = async (user = {}) => {
if (!user.email) { validation.validateOrThrow(schemas.create, user);
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`);
}
// Check if user is already registered
const existingUser = await getByEmail(user.email); const existingUser = await getByEmail(user.email);
if (existingUser) { if (existingUser) {
throw new Error( throw utils.customError(
`A user with email "${user.email}" is already registered` `A user with email "${user.email}" is already registered`
); );
} }
@@ -40,16 +33,10 @@ const register = async (user = {}) => {
/** /**
* Get user by email address * Get user by email address
* @param {string} email * @param {string} email Email address of user to retrieve
*/ */
const getByEmail = async (email) => { const getByEmail = async (email) => {
if (!email) { validation.validateOrThrow(sharedSchemas.email, email);
throw new Error(`"email" is required`);
}
if (!utils.validateEmailAddress(email)) {
throw new Error(`"${email}" is not a valid email address`);
}
let user = await db.getByKey(email); let user = await db.getByKey(email);
@@ -61,11 +48,8 @@ const getByEmail = async (email) => {
* Get user by id * Get user by id
* @param {string} id * @param {string} id
*/ */
const getById = async (id) => { const getById = async (id) => {
if (!id) { validation.validateOrThrow(sharedSchemas.id, id);
throw new Error(`"id" is required`);
}
let user = await db.getById(`user`, id); let user = await db.getById(`user`, id);
@@ -97,7 +81,7 @@ const convertToPublicFormat = (user = {}) => {
}; };
module.exports = { module.exports = {
register, create,
getByEmail, getByEmail,
getById, getById,
convertToPublicFormat, convertToPublicFormat,

View File

@@ -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"]
}
};

16
api/models/validation.js Normal file
View File

@@ -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,
};

View File

@@ -4,6 +4,7 @@
"description": "", "description": "",
"main": "app.js", "main": "app.js",
"dependencies": { "dependencies": {
"ajv": "^6.12.6",
"aws-sdk": "^2.792.0", "aws-sdk": "^2.792.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"express": "^4.17.1", "express": "^4.17.1",

View File

@@ -5,16 +5,30 @@
const bcrypt = require(`bcryptjs`); 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) => { const customError = (message, code) => {
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,}))$/; const error = new Error(message);
return re.test(String(email).toLowerCase()); 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 * Hash password
* @param {*} user * @param {string} password Password to hash
*/ */
const hashPassword = (password) => { const hashPassword = (password) => {
const salt = bcrypt.genSaltSync(10); const salt = bcrypt.genSaltSync(10);
@@ -23,13 +37,16 @@ const hashPassword = (password) => {
/** /**
* Compare password * Compare password
* @param {string} candidatePassword Hashed password supplied by user
* @param {string} trustedPassword Hashed password on record for user
*/ */
const comparePassword = (candidatePassword, trustedPassword) => { const comparePassword = (candidatePassword, trustedPassword) => {
return bcrypt.compareSync(candidatePassword, trustedPassword); return bcrypt.compareSync(candidatePassword, trustedPassword);
}; };
module.exports = { module.exports = {
customError,
validationError,
hashPassword, hashPassword,
comparePassword, comparePassword,
validateEmailAddress,
}; };