Refactor with base actions methods

This commit is contained in:
2021-01-25 23:28:04 -06:00
parent 5f23b5b633
commit 9d98ad4e84
11 changed files with 204 additions and 222 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = {

View File

@@ -1,5 +0,0 @@
const users = require(`./users`);
module.exports = {
users,
};

View File

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

View File

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

View File

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

View File

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