Refactor with base actions methods
This commit is contained in:
@@ -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));
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
61
api/controllers/base/index.js
Normal file
61
api/controllers/base/index.js
Normal 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,
|
||||
};
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
59
api/controllers/users/utils.js
Normal file
59
api/controllers/users/utils.js
Normal 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,
|
||||
}
|
||||
@@ -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 = {
|
||||
@@ -1,5 +0,0 @@
|
||||
const users = require(`./users`);
|
||||
|
||||
module.exports = {
|
||||
users,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user