From 9d98ad4e84c285087cdc9f2aaea11afb81d92aa5 Mon Sep 17 00:00:00 2001 From: benjaminramey Date: Mon, 25 Jan 2021 23:28:04 -0600 Subject: [PATCH] Refactor with base actions methods --- api/config/passport.js | 7 +-- api/controllers/base/index.js | 61 +++++++++++++++++++++ api/controllers/users/index.js | 85 +++++++++++++---------------- api/controllers/users/schemas.js | 17 ++++++ api/controllers/users/utils.js | 59 +++++++++++++++++++++ api/{models/db.js => db/index.js} | 14 +++-- api/models/index.js | 5 -- api/models/users/index.js | 88 ------------------------------- api/models/users/schemas.js | 21 -------- api/utils/index.js | 52 ------------------ api/validation/index.js | 17 +++++- 11 files changed, 204 insertions(+), 222 deletions(-) create mode 100644 api/controllers/base/index.js create mode 100644 api/controllers/users/utils.js rename api/{models/db.js => db/index.js} (66%) delete mode 100644 api/models/index.js delete mode 100644 api/models/users/index.js delete mode 100644 api/models/users/schemas.js delete mode 100644 api/utils/index.js diff --git a/api/config/passport.js b/api/config/passport.js index 2fd3ff3..a68a18e 100644 --- a/api/config/passport.js +++ b/api/config/passport.js @@ -4,7 +4,8 @@ const StrategyJWT = require(`passport-jwt`).Strategy; const ExtractJWT = require(`passport-jwt`).ExtractJwt; -const { users } = require(`../models`); +const userUtils = require(`../controllers/users/utils`); +const db = require(`../db`); module.exports = (passport) => { const options = {}; @@ -15,7 +16,7 @@ module.exports = (passport) => { new StrategyJWT(options, async (jwtPayload, done) => { let user; try { - user = await users.getById(jwtPayload.id); + user = await db.getById(`user`, jwtPayload.id); } catch (error) { console.log(error); return done(error, null); @@ -24,7 +25,7 @@ module.exports = (passport) => { if (!user) { return done(null, false); } - return done(null, users.convertToPublicFormat(user)); + return done(null, userUtils.convertToPublicFormat(user)); }) ); }; diff --git a/api/controllers/base/index.js b/api/controllers/base/index.js new file mode 100644 index 0000000..a8235f6 --- /dev/null +++ b/api/controllers/base/index.js @@ -0,0 +1,61 @@ +/* + * Controller base actions + * + * This module contains base actions that follow + * a template for CRUD operations on a DB and (optionally) + * do custom things afterward to return a response. + */ + +const validation = require(`../../validation`); +const db = require(`../../db`); + +const createAction = async (req, res, config) => { + validation.validateOrThrow(config.validationSchema, req.body); + + const key = config.getObjectKey(); + let existingObject = await db.getByKey(key); + if (existingObject) { + throw customError( + `An object with key "${key}" already exists in the database.`, + 400 + ); + } + + const dbObject = config.buildDbObject(req.body); + await db.put(config.dbKind, dbObject); + const createdObject = await db.getByKey(key); + + const responseBody = config.buildResponseBody(createdObject); + res.json(responseBody); +}; + +const getByKeyAction = async (req, res, config) => { + validation.validateOrThrow(config.validationSchema, req.body); + const key = config.getObjectKey(); + let existingObject = await db.getByKey(key); + + if (!existingObject) { + return res + .status(404) + .send({ error: config.notFoundMessage }); + } + + const responseBody = config.buildResponseBody(existingObject); + res.json(responseBody); +}; + +/** + * Build a custom error + * @param {string} message Error message + * @param {number} code Error code, should be HTTP status code + */ +const customError = (message = "An error occurred.", code = 500) => { + const error = new Error(message); + error.code = code; + return error; +}; + +module.exports = { + createAction, + getByKeyAction, +}; diff --git a/api/controllers/users/index.js b/api/controllers/users/index.js index 451cbd7..fadd5c1 100644 --- a/api/controllers/users/index.js +++ b/api/controllers/users/index.js @@ -2,11 +2,9 @@ * Controllers: Users */ -const jwt = require(`jsonwebtoken`); -const users = require(`../../models/users`); -const { comparePassword } = require(`../../utils`); -const validation = require(`../../validation`); const schemas = require(`./schemas`); +const base = require(`../base`); +const userUtils = require(`./utils`); /** * Save @@ -14,22 +12,24 @@ const schemas = require(`./schemas`); * @param {*} res */ 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), - process.env.tokenSecret, - { - expiresIn: 604800, // 1 week + const config = { + validationSchema: schemas.create, + getObjectKey: () => req.body.email, + buildDbObject: () => { + return { + hk: req.body.email, + password: userUtils.hashPassword(req.body.password) + }; + }, + dbKind: `user`, + buildResponseBody: createdUser => { + return { + message: `Authentication successful`, + token: userUtils.getToken(createdUser), + }; } - ); - - res.json({ - message: `Authentication successful`, - token, - }); + }; + await base.createAction(req, res, config); }; /** @@ -38,35 +38,26 @@ const register = async (req, res) => { * @param {*} res */ const login = async (req, res) => { - validation.validateOrThrow(schemas.login, req.body); - - let user = await users.getByEmail(req.body.email); - - if (!user) { - return res - .status(404) - .send({ error: `Authentication failed. User not found.` }); - } - - const isCorrect = comparePassword(req.body.password, user.password); - if (!isCorrect) { - return res - .status(401) - .send({ error: `Authentication failed. Wrong password.` }); - } - - const token = jwt.sign( - users.convertToPublicFormat(user), - process.env.tokenSecret, - { - expiresIn: 604800, // 1 week + const config = { + validationSchema: schemas.login, + getObjectKey: () => req.body.email, + notFoundMessage: `Authentication failed. User not found.`, + buildResponseBody: user => { + const isCorrect = userUtils.comparePassword(req.body.password, user.password); + if (!isCorrect) { + return res + .status(401) + .send({ error: `Authentication failed. Wrong password.` }); + } + + return { + message: `Authentication successful`, + token: userUtils.getToken(user), + }; } - ); + }; - res.json({ - message: `Authentication successful`, - token, - }); + await base.getByKeyAction(req, res, config); }; /** @@ -75,7 +66,7 @@ const login = async (req, res) => { * @param {*} res */ const get = async (req, res) => { - const user = users.convertToPublicFormat(req.user); + const user = userUtils.convertToPublicFormat(req.user); res.json({ user }); }; diff --git a/api/controllers/users/schemas.js b/api/controllers/users/schemas.js index 624aa3f..8900cf2 100644 --- a/api/controllers/users/schemas.js +++ b/api/controllers/users/schemas.js @@ -14,6 +14,23 @@ const login = { required: ["email", "password"], }; +const create = { + $schema: "http://json-schema.org/draft-07/schema#", + title: "Create User", + description: "An object to create a new user", + type: "object", + properties: { + email: commonSchemas.email, + password: { + type: "string", + minLength: 10, + maxLength: 50, + }, + }, + required: ["email", "password"], +}; + module.exports = { login, + create }; diff --git a/api/controllers/users/utils.js b/api/controllers/users/utils.js new file mode 100644 index 0000000..11b29bc --- /dev/null +++ b/api/controllers/users/utils.js @@ -0,0 +1,59 @@ +/* + * User utilities + */ + +const bcrypt = require(`bcryptjs`); +const jwt = require(`jsonwebtoken`); + +const getToken = (user) => { + return jwt.sign( + convertToPublicFormat(user), + process.env.tokenSecret, + { + expiresIn: 604800, // 1 week + } + ); +}; + +const convertToPublicFormat = (user = {}) => { + user.email = user.email || user.hk || null; + user.id = user.id || user.sk2 || null; + if (user.hk) { + delete user.hk; + } + if (user.sk) { + delete user.sk; + } + if (user.sk2) { + delete user.sk2; + } + if (user.password) { + delete user.password; + } + return user; +}; + +/** + * Hash password + * @param {string} password Password to hash + */ +const hashPassword = (password) => { + const salt = bcrypt.genSaltSync(10); + return bcrypt.hashSync(password, salt); +}; + +/** + * 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 = { + getToken, + convertToPublicFormat, + hashPassword, + comparePassword, +} diff --git a/api/models/db.js b/api/db/index.js similarity index 66% rename from api/models/db.js rename to api/db/index.js index fd380be..f639496 100644 --- a/api/models/db.js +++ b/api/db/index.js @@ -1,5 +1,7 @@ const AWS = require(`aws-sdk`); const shortid = require(`shortid`); +const commonSchemas = require(`../validation/commonSchemas`); +const validation = require(`../validation`); const dynamodb = new AWS.DynamoDB.DocumentClient({ region: process.env.AWS_REGION, @@ -31,21 +33,25 @@ const getByKey = async (key) => { ExpressionAttributeValues: { ":hk": key }, }; - return await dynamodb.query(params).promise(); + const obj = await dynamodb.query(params).promise(); + return obj.Items && obj.Items[0] ? obj.Items[0] : null; }; -const getById = async (type, id) => { +const getById = async (kind, id) => { + validation.validateOrThrow(commonSchemas.id, id); + const params = { ...defaultParams, IndexName: process.env.dbIndex1, KeyConditionExpression: `sk2 = :sk2 and sk = :sk`, ExpressionAttributeValues: { ":sk2": id, - ":sk": type, + ":sk": kind, }, }; - return await dynamodb.query(params).promise(); + const obj = await dynamodb.query(params).promise(); + return obj.Items && obj.Items[0] ? obj.Items[0] : null; }; module.exports = { diff --git a/api/models/index.js b/api/models/index.js deleted file mode 100644 index ce3f1b6..0000000 --- a/api/models/index.js +++ /dev/null @@ -1,5 +0,0 @@ -const users = require(`./users`); - -module.exports = { - users, -}; diff --git a/api/models/users/index.js b/api/models/users/index.js deleted file mode 100644 index 62046e9..0000000 --- a/api/models/users/index.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Model: Users - */ -const utils = require(`../../utils`); -const db = require(`../db`); -const schemas = require(`./schemas`); -const commonSchemas = require(`../../validation/commonSchemas`); -const validation = require(`../../validation`); - -/** - * Create a new user - * @param {string} user.email User email - * @param {string} user.password User password - */ -const create = async (user = {}) => { - validation.validateOrThrow(schemas.create, user); - - const existingUser = await getByEmail(user.email); - if (existingUser) { - throw utils.customError( - `A user with email "${user.email}" is already registered`, - 400 - ); - } - - user.password = utils.hashPassword(user.password); - - await db.put(`user`, { - hk: user.email, - password: user.password, - }); -}; - -/** - * Get user by email address - * @param {string} email Email address of user to retrieve - */ -const getByEmail = async (email) => { - validation.validateOrThrow(commonSchemas.email, email); - - let user = await db.getByKey(email); - - user = user.Items && user.Items[0] ? user.Items[0] : null; - return user; -}; - -/** - * Get user by id - * @param {string} id - */ -const getById = async (id) => { - validation.validateOrThrow(commonSchemas.id, id); - - let user = await db.getById(`user`, id); - - user = user.Items && user.Items[0] ? user.Items[0] : null; - return user; -}; - -/** - * Convert user record to public format - * This hides the keys used for the dynamodb's single table design and returns human-readable properties. - * @param {*} user - */ -const convertToPublicFormat = (user = {}) => { - user.email = user.email || user.hk || null; - user.id = user.id || user.sk2 || null; - if (user.hk) { - delete user.hk; - } - if (user.sk) { - delete user.sk; - } - if (user.sk2) { - delete user.sk2; - } - if (user.password) { - delete user.password; - } - return user; -}; - -module.exports = { - create, - getByEmail, - getById, - convertToPublicFormat, -}; diff --git a/api/models/users/schemas.js b/api/models/users/schemas.js deleted file mode 100644 index 3337a35..0000000 --- a/api/models/users/schemas.js +++ /dev/null @@ -1,21 +0,0 @@ -const commonSchemas = require(`../../validation/commonSchemas`); - -const create = { - $schema: "http://json-schema.org/draft-07/schema#", - title: "Create User", - description: "An object to create a new user", - type: "object", - properties: { - email: commonSchemas.email, - password: { - type: "string", - minLength: 10, - maxLength: 50, - }, - }, - required: ["email", "password"], -}; - -module.exports = { - create, -}; diff --git a/api/utils/index.js b/api/utils/index.js deleted file mode 100644 index 8f8da7b..0000000 --- a/api/utils/index.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Utils - */ - -const bcrypt = require(`bcryptjs`); - -/** - * Build a custom error - * @param {string} message Error message - * @param {number} code Error code, should be HTTP status code - */ -const customError = (message = "An error occurred.", code = 500) => { - 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 {string} password Password to hash - */ -const hashPassword = (password) => { - const salt = bcrypt.genSaltSync(10); - return bcrypt.hashSync(password, salt); -}; - -/** - * 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, -}; diff --git a/api/validation/index.js b/api/validation/index.js index 1b6b15e..37eeddc 100644 --- a/api/validation/index.js +++ b/api/validation/index.js @@ -1,6 +1,8 @@ -const Ajv = require(`ajv`); +/* + * Validation + */ -const { validationError } = require(`../utils`); +const Ajv = require(`ajv`); const validateOrThrow = (schema, data) => { const ajv = new Ajv(); @@ -11,6 +13,17 @@ const validateOrThrow = (schema, data) => { } }; +/** + * 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; +}; + module.exports = { validateOrThrow, };