Skip to content

Commit

Permalink
feat(create_article): add functionality to create articles
Browse files Browse the repository at this point in the history
- setup middlewares to validate article details and to authenticate user token
- create database models for articles
- setup routes for the endpoint to post an article
- write test for testing the functionality to create an article
- add documentation for this endpoint
[Delievers #164139686]
  • Loading branch information
kleva-j committed Mar 7, 2019
1 parent 7b7600c commit 8f9850f
Show file tree
Hide file tree
Showing 19 changed files with 1,058 additions and 310 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ mochawesome-report
# Ignore yarn generated files
yarn.lock
dist/

#coveralls code coverage reports
.nyc_output/
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ language: node_js
script: npm run test
node_js:
- "10"
services:
- postgresql
before_script:
- psql -c 'create database vidarapp;' -U postgres
script:
- npm install && npm install sequelize-cli -g
- npm run test
after_success:
- npm run coverage
30 changes: 30 additions & 0 deletions controllers/articles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Article } from '../models';

/**
* @class ArticleController
* @override
* @export
*/
export default class ArticleController {
/**
* @description - Create a new article
* @static
* @param {Object} req - the request object
* @param {Object} res - the response object
* @memberof ArticleController
* @returns {Object} class instance
*/
static async createArticle(req, res) {
const { title, description, body } = req.body;
try {
const result = await Article.create({ title, description, body });
return res.status(201).json({
success: true,
message: 'New article created successfully',
article: result
});
} catch (error) {
return res.status(500).json({ success: false, error: [error.message] });
}
}
}
9 changes: 0 additions & 9 deletions controllers/user.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import dotenv from 'dotenv';
import jwt from 'jsonwebtoken';
import { User } from '../models';
// import sendMail from '../helpers/emails';

dotenv.config();
const { JWT_SECRET } = process.env;
Expand All @@ -11,23 +10,18 @@ const generateToken = id => jwt.sign(
{ expiresIn: '24h' },
);


/**
* @class UserController
* @override
* @export
*
*/
export default class UserController {
/**
* @description - Creates a new user
* @static
*
* @param {object} req - HTTP Request
* @param {object} res - HTTP Response
*
* @memberof UserController
*
* @returns {object} Class instance
*/
static registerUser(req, res) {
Expand Down Expand Up @@ -66,12 +60,9 @@ export default class UserController {
/**
* @description - Verifies a user's account
* @static
*
* @param {object} req - HTTP Request
* @param {object} res - HTTP Response
*
* @memberof UserController
*
* @returns {object} Class instance
*/
static verifyAccount(req, res) {
Expand Down
27 changes: 25 additions & 2 deletions doc.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"url": "https://www.gnu.org/licenses/gpl-3.0.en.html"
}
},
"host": "https://vidar-ah-backend-staging.herokuapp.com",
"host": "vidar-ah-backend-staging.herokuapp.com",
"basePath": "/api/v1",
"schemes": [
"https",
Expand All @@ -23,7 +23,7 @@
"/api/v1/user": {
"post": {
"tags": [
"authentication"
"Authentication"
],
"summary": "User registration",
"description": "A Social platform for the creative at heart",
Expand All @@ -45,6 +45,29 @@
}
}
}
},
"/api/v1/article": {
"post": {
"tags": [
"Articles"
],
"summary": "Create an article",
"description": "A Social platform for the creative at heart",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"responses": {
"201": {
"description": "New article created successfully"
},
"422": {
"description": "Validation error"
}
}
}
}
},
"externalDocs": {
Expand Down
3 changes: 3 additions & 0 deletions helpers/slug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import slug from 'slug';

export default validString => slug(validString, { lower: true });
36 changes: 36 additions & 0 deletions middleware/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import jwt from 'jsonwebtoken';

const { JWT_SECRET } = process.env;
/**
* Authentication class
*/
class Auth {
/**
* @description Middleware function to verify if user has a valid token
* @param {object} req http request object
* @param {object} res http response object
* @param {Function} next next middleware function
* @returns {undefined}
*/
static verifyUser(req, res, next) {
const token = req.headers['x-access-token'] || req.query.token || req.headers.authorization;
if (!token) {
return res.status(401).json({
success: false,
message: 'Unauthorized! You are required to be logged in to perform this operation.',
});
}
jwt.verify(token, JWT_SECRET, (err, decoded) => {
if (err) {
return res.status(401).json({
success: false,
message: 'Your session has expired, please login again to continue',
});
}
req.user = decoded;
return next();
});
}
}

export default Auth;
18 changes: 18 additions & 0 deletions middleware/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,21 @@ export const validateSignup = [
.custom(value => !/\s/.test(value))
.withMessage('No spaces are allowed in the username.'),
];

export const validateArticle = [
check('title')
.exists()
.withMessage('Article should have a title.'),

check('description')
.exists()
.withMessage('Article should have a description.')
.isLength({ min: 6 })
.withMessage('Description must be at least 6 characters long.'),

check('body')
.exists()
.withMessage('Article should have a body.')
.isLength({ min: 6 })
.withMessage('Article should have a body with at least 6 characters long.'),
];
39 changes: 39 additions & 0 deletions middleware/verifyUser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { User } from '../models';

export default (req, res, next) => {
const {
body,
user,
} = req;
const options = {};
if (user) {
options.fieldName = 'id';
options.fieldValue = user.id;
} else if (body) {
options.fieldName = 'email';
options.fieldValue = body.email;
}

const { fieldName, fieldValue } = options;
User.findOne({ where: { [fieldName]: fieldValue } })
.then((foundUser) => {
if (foundUser) {
const { dataValues: { verified } } = foundUser;
if (!verified) {
return res.status(403).json({
success: false,
errors: [
'User has not been verified.'
]
});
}
return next();
}
return res.status(404).json({
success: false,
errors: [
'User not found.'
]
});
});
};
54 changes: 54 additions & 0 deletions migrations/20190305085324-create-article.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@

module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('Articles', {
id: {
unique: true,
allowNull: false,
primaryKey: true,
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4
},
title: {
type: Sequelize.STRING,
allowNull: {
args: true,
msg: 'Article should have a title'
}
},
slug: {
type: Sequelize.STRING,
allowNull: false,
unique: {
args: true,
msg: 'Article should have a unique slug'
}
},
description: {
type: Sequelize.TEXT,
allowNull: {
args: true,
msg: 'Article should have a description'
}
},
body: {
type: Sequelize.TEXT,
allowNull: {
args: true,
msg: 'Article should have a body'
}
},
taglist: {
type: Sequelize.ARRAY(Sequelize.STRING),
defaultValue: []
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: queryInterface => queryInterface.dropTable('Articles')
};
13 changes: 11 additions & 2 deletions models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,19 @@ module.exports = (sequelize, DataTypes) => {
allowNull: true,
},
};
const User = sequelize.define('User', userSchema);

const User = sequelize.define('User', userSchema, {
classMethods: {
associate(models) {
User.hasMany(models.Article);
}
}
});
User.hook('beforeValidate', (user) => {
user.verificationId = shortId.generate();
user.password = bcrypt.hashSync(user.password, bcrypt.genSaltSync(10));
if (user.password) {
user.password = bcrypt.hashSync(user.password, bcrypt.genSaltSync(10));
}
});
User.hook('afterCreate', (user) => {
const { email, name, verificationId } = user;
Expand Down
65 changes: 65 additions & 0 deletions models/article.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import shortId from 'shortid';
import generateSlug from '../helpers/slug';

module.exports = (sequelize, DataTypes) => {
const Article = sequelize.define('Article', {
id: {
unique: true,
allowNull: false,
primaryKey: true,
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4
},
title: {
type: DataTypes.STRING,
allowNull: {
args: true,
msg: 'Article should have a title'
}
},
slug: {
type: DataTypes.STRING,
allowNull: false,
unique: {
args: true,
msg: 'Article should have a unique slug'
}
},
description: {
type: DataTypes.TEXT,
allowNull: {
args: true,
msg: 'Article should have a description'
}
},
body: {
type: DataTypes.TEXT,
allowNull: {
args: true,
msg: 'Article should have a body'
}
},
taglist: {
type: DataTypes.ARRAY(DataTypes.STRING),
defaultValue: []
}
}, {
classMethods: {
associate(models) {
Article.belongsTo(models.User, {
foreignKey: 'userId',
onDelete: 'CASCADE',
});
}
}
});

Article.hook('beforeValidate', (article) => {
article.slug = (`${generateSlug(article.title)}-${shortId.generate()}`).toLowerCase();
article.title = article.title.trim();
article.description = article.description.trim();
article.body = article.body.trim();
});

return Article;
};
Loading

0 comments on commit 8f9850f

Please sign in to comment.