Skip to content

Commit

Permalink
Merge pull request #20 from andela/ft_159206054_user_create_articles
Browse files Browse the repository at this point in the history
#159206054 User can create articles
  • Loading branch information
anuonifade committed Aug 21, 2018
2 parents adb1716 + 2ed2ba5 commit f80bcb2
Show file tree
Hide file tree
Showing 24 changed files with 1,363 additions and 57 deletions.
8 changes: 8 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ SECRETE_KEY=
VERIFYTOKEN_EXPIRY=
URL_HOST=
NO_REPLY_MAIL=
CLOUD_NAME=
API_KEY=
API_SECRET=

GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
FACEBOOK_APP_ID=
FACEBOOK_APP_SECRET=
10 changes: 10 additions & 0 deletions config/cloudinary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import cloudinary from 'cloudinary';

// configure cloudinary to be able to upload image
cloudinary.config({
cloud_name: process.env.CLOUD_NAME,
api_key: process.env.API_KEY,
api_secret: process.env.API_SECRET,
});

export default cloudinary;
3 changes: 0 additions & 3 deletions config/index.js

This file was deleted.

163 changes: 163 additions & 0 deletions controllers/ArticleController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import cloudinary from '../config/cloudinary';
import Utilities from '../helpers/utilities';
import { Article, User } from '../models';
import createArticleHelper from '../helpers/createArticleHelper';

/**
* Article class for users
* @param {method} createArticle - Create article
* @param {method} getArticle - Get a single article
* @param {method} editArticle update a single article
*/
class ArticleController {
/**
* Create an article for a user
* @param {object} req - The request object
* @param {object} res - The response object sent to user
* @return {object} A object containing created articles.
*/
static createArticle(req, res) {
const {
title, description, body, tagList, imageData,
} = req.body.article;

const { userId } = req;

const articleObject = {
title, description, body, tagList, imageData, userId
};
/**
* check if image was provided in the request
* upload the image to cloudinary, save the article
* with the cloudinary URL in database but if an error
* was encountered from cloudinary go ahead and create the article
*/
if (imageData) {
return cloudinary.v2.uploader.upload(imageData, { tags: 'basic_sample' })
.then(image => createArticleHelper(res, articleObject, image.url))
.catch(() => createArticleHelper(res, articleObject));
}

// if there no image was provided go ahead to create the article
return createArticleHelper(res, articleObject);
}

/**
* get an article using slug as query parameter
* @param {object} req - request object
* @param {object} res - response object
* @param {function} next - next function
* @returns {object} - the found article from database or error if not found
*/
static getArticle(req, res, next) {
const { slug } = req.params;

return Article
.findOne({
where: { slug, },
include: [{
model: User,
attributes: { exclude: ['id', 'email', 'hashedPassword', 'createdAt', 'updatedAt'] }
}],
attributes: { exclude: ['userId'] }
})
.then((article) => {
// if the article does not exist
if (!article) {
return res.status(404).json({
errors: {
body: [
'Ooops! the article cannot be found.'
]
}
});
}

return res.status(200).json({ article });
})
.catch(next);
}

/**
* get all articles created
* @param {object} req - request object
* @param {object} res - response object
* @param {function} next - next function
* @returns {object} - the found article from database or empty if not found
*/
static listAllArticles(req, res, next) {
return Article
.findAll({
include: [{
model: User,
attributes: { exclude: ['id', 'email', 'hashedPassword', 'createdAt', 'updatedAt'] }
}],
attributes: { exclude: ['userId'] }
})
.then((articles) => {
// check if there was no article created
if (articles.length === 0) {
return res.status(200).json({
message: 'No articles created',
articles,
});
}

return res.status(200).json({ articles, articlesCount: articles.length });
})
.catch(next);
}

/**
* @function editArticle
* @summary: API controller to handle requests to edit an article
* @param {object} req: request object
* @param {object} res: response object
* @param {function} next - next function
* @returns {object} api response: article object for
* successful requests, or error object for
* requests that fail
*/
static editArticle(req, res, next) {
const { title, description, body } = req.body.article;
const { count } = req;
const { slug } = req.params;
return Article.update({
title,
description,
body,
updatedCount: Utilities.increaseCount(count)
}, {
where: {
slug,
},
returning: true,
plain: true
})
.then(result => res.status(200).json({
success: true,
article: result[1]
}))
.catch(next);
}

/**
* @function deleteArticle
* @summary: API controller to handle requests to delete an article
* @param {object} req: request object
* @param {object} res: response object
* @param {function} next - next function
* @returns {object} api response: article object for
* successful requests, or error object for requests that fail
*/
static deleteArticle(req, res, next) {
const { slug } = req.params;
Article.destroy({
where: { slug }
})
.then(() => res.status(200).json({ message: 'Article successfully deleted' }))
.catch(next);
}
}

export default ArticleController;
4 changes: 2 additions & 2 deletions controllers/UsersController.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default class UsersController {
}

/**
* Checks if the user exists and returns a JWT token
* Checks if the user exists and returns JWT token
* @param {*} req - request object
* @param {*} res - response object
* @param {*} next - Next function
Expand Down Expand Up @@ -118,7 +118,7 @@ export default class UsersController {
* @summary Return a user's profile after updating it
* @param {object} req - Request object
* @param {object} res - Response object
* @param {function} next - For errors
* @param {function} next - next function
* @returns {object} An object containing all the data related to the user if update successful
*/
static editProfile(req, res, next) {
Expand Down
38 changes: 38 additions & 0 deletions helpers/createArticleHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import generateUniqueSlug from './generateUniqueSlug';
import { Article, User } from '../models';


/**
* @description an helper function to help create article in database
* @param {object} res - response object
* @param {object} articleObject - contains extracted article fields
* @param {string} imageUrl - image url from cloudinary
* @returns object - the created article from the database
*/

const createArticleHelper = (res, articleObject, imageUrl = null) => {
const {
title, description, body, tagList, userId
} = articleObject;

return Article
.create({
slug: generateUniqueSlug(title),
title,
description,
body,
userId,
tagList,
imageUrl,
})
.then(article => Article.findById(article.id, {
include: [{
model: User,
attributes: { exclude: ['id', 'email', 'hashedPassword', 'createdAt', 'updatedAt'] }
}],
attributes: { exclude: ['userId'] }
}))
.then(article => res.status(201).json({ article }));
};

export default createArticleHelper;
14 changes: 14 additions & 0 deletions helpers/generateUniqueSlug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import slugify from 'slugify';
import cuid from 'cuid';

/** create a string as slug to make articles unique
* @param {string} title - title of the article from request body
* @returns {string} slug - timestamped string with randomly generated slug to avoid collision
*/
const generateUniqueSlug = (title) => {
const sluggedTitle = slugify(title);
const slug = `${sluggedTitle}-${cuid()}`;
return slug.toLowerCase();
};

export default generateUniqueSlug;
3 changes: 1 addition & 2 deletions helpers/sendmail.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import nodemailer from 'nodemailer';
import jwt from 'jsonwebtoken';
import winston from 'winston';
import mailtemplate from './mailtemplate';

exports.sendEmail = async (userToBeVerified) => {
Expand Down Expand Up @@ -33,6 +32,6 @@ exports.sendEmail = async (userToBeVerified) => {
}
});
// setup email data with unicode symbols
transporter.sendMail(mailOptions).then(result => winston.info(result));
transporter.sendMail(mailOptions);
});
};
26 changes: 20 additions & 6 deletions helpers/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ import stubTransport from 'nodemailer-stub-transport';
import { User } from '../models';


/** Class representing all utility functions */
export default class Utilities {
/**
* Converts payload to JWT
* @param {Object} payload - Object to convert to JWT
* @returns {string} token - JWT String created.
* Class representing all utility functions
*
*/
export default class Utilities {
/**
* Converts payload to JWT
* @param {Object} payload - Object to convert to JWT
* @returns {string} token - JWT String created.
*/
static signToken(payload) {
try {
return jwt.sign(payload, process.env.JWT_TOKEN_SECRET, { expiresIn: '24h' });
return jwt.sign(payload, process.env.JWT_TOKEN_SECRET, { expiresIn: '24h' }).toString();
} catch (err) {
return err;
}
Expand Down Expand Up @@ -137,4 +140,15 @@ export default class Utilities {
})
.catch(next);
}

/**
* @function increaseCount
* @summary: A funtion to increase count
* each time an article is updated
* @param {Integer} count: input param
* @returns {Integer} number of count: for updating articles
*/
static increaseCount(count) {
if (Number.isInteger(count)) return count + 1;
}
}
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import methodOverride from 'method-override';
import morgan from 'morgan';
import debugLog from 'debug';
import expressValidator from 'express-validator';
import { } from 'dotenv/config';

import { } from 'dotenv/config';
import passportConfig from './config/passport';
import routes from './routes';

Expand Down
45 changes: 45 additions & 0 deletions middlewares/checkUser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import db from '../models/index';

export const articleExists = (req, res, next) => {
const { slug } = req.params;
db.Article.find({
where: { slug }
})
.then((foundArticle) => {
if (!foundArticle) {
return res.status(404).json({
errors: {
body: ['this article does not exist']
}
});
}
if (req.userId !== foundArticle.userId) {
return res.status(404).json({
errors: {
body: ['Not enough permission to perform this operartion']
}
});
}
req.count = foundArticle.updatedCount;
next();
})
.catch((err) => {
res.status(500).json({
error: err
});
});
};

export const checkCount = (req, res, next) => {
const { count } = req;
if (count > 2) {
return res.status(403).json({
errors: {
body: [
'this article has exceeded its edit limit'
]
}
});
}
next();
};
4 changes: 3 additions & 1 deletion middlewares/getUser.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import db from '../models';

const { User } = db;
const getUser = (req, res, next) => {
db.User.findById(req.userId)
User.findById(req.userId)
.then((user) => {
if (!user || user.rowCount === 0) {
return res.status(404).json({
Expand All @@ -13,6 +14,7 @@ const getUser = (req, res, next) => {
}
req.userObject = user;
next();
return null;
})
.catch(next);
};
Expand Down
Loading

0 comments on commit f80bcb2

Please sign in to comment.