diff --git a/src/api/controllers/adminPermissionsController.js b/src/api/controllers/adminPermissionsController.js index 60ce2a68..b2e4d5aa 100644 --- a/src/api/controllers/adminPermissionsController.js +++ b/src/api/controllers/adminPermissionsController.js @@ -2,7 +2,11 @@ import models from '../models'; import status from '../../helpers/constants/status.codes'; import paginateUser from '../../helpers/generate.pagination.details'; -const { User } = models; +const generatePagination = paginateUser; + +const { + Article, Report, User, Reason +} = models; /** * containing all user's model controllers (signup, login) * @@ -62,7 +66,7 @@ export default class adminPermissions { const { email } = req.params; const { firstName, lastName } = req.body; - const result = await User.update({ + await User.update({ email, firstName, lastName @@ -207,4 +211,155 @@ export default class adminPermissions { } }); } + + /** + * fetch all reported Articles + * + * @static + * @param {array} req - request array + * @param {array} res - respond array + * @returns {array} response body + * @memberof getArticles + */ + static async getReportedArticles(req, res) { + const defaultOffset = 0; + const defaultLimit = 10; + const offset = req.query.offset || defaultOffset; + const limit = req.query.limit || defaultLimit; + const paginatedArticle = await Report.findAll({ + attributes: ['createdAt'], + include: [ + { + model: Article, + required: true, + attributes: ['title', 'slug', 'description', 'body', 'coverImage'] + }, + { + model: User, + required: true, + attributes: ['username', 'email'] + }, + { + model: Reason, + required: true, + attributes: ['description'] + } + ], + offset, + limit + }); + res.status(status.OK).send({ + status: status.OK, + paginationDetails: generatePagination(paginatedArticle.length, + paginatedArticle, + offset, + limit), + data: paginatedArticle + }); + } + + /** + * fetch single reported Article + * + * @static + * @param {array} req - request array + * @param {array} res - respond array + * @returns {array} response body + * @memberof getArticles + */ + static async getReportedArticle(req, res) { + const { slug } = req.params; + const reportedArticle = await Report.findOne({ + attributes: [['createdAt', 'ReportedOn']], + where: { + reportedArticleSlug: slug + }, + include: [ + { + model: Article, + required: true, + attributes: ['title', 'slug', 'description', 'body', 'coverImage'] + }, + { + model: User, + required: true, + attributes: ['username', 'email'] + }, + { + model: Reason, + required: true, + attributes: ['description'] + } + ] + }); + res.status(status.OK).send({ + status: status.OK, + data: reportedArticle + }); + } + + /** + * block Article + * + * @static + * @param {object} req - request body + * @param {object} res - response body + * @returns { object } - response + * @memberof blockArticle + */ + static async blockArticle(req, res) { + const { slug } = req.params; + const { title, body } = req; + const isBlocked = true; + + await Article.update({ + isBlocked + }, + { + where: { + slug + } + }); + return res.status(status.OK).json({ + status: status.OK, + message: `${slug} blocked successfully`, + data: { + title, + slug, + body + } + }); + } + + /** + * unblock Article + * + * @static + * @param {object} req - request body + * @param {object} res - response body + * @returns { object } - response + * @memberof blockArticle + */ + static async unBlockArticle(req, res) { + const { slug } = req.params; + const isBlocked = 'false'; + const { title, body } = req; + await Article.update({ + isBlocked + }, + { + where: { + slug + } + }); + return res.status(status.OK).json({ + status: status.OK, + message: `${slug} unblocked successfully`, + data: { + title, + slug, + body + } + }); + } } diff --git a/src/api/controllers/articleController.js b/src/api/controllers/articleController.js index 55434301..36f2300d 100644 --- a/src/api/controllers/articleController.js +++ b/src/api/controllers/articleController.js @@ -92,17 +92,28 @@ export default class ArticleController { const articles = await Article.findAndCountAll({ offset, limit, - attributes: ['id', 'title', 'description', 'body', 'slug', 'coverImage', 'tagList', 'createdAt', 'updatedAt'], + attributes: [ + 'id', + 'title', + 'description', + 'body', + 'slug', + 'coverImage', + 'tagList', + 'createdAt', + 'updatedAt' + ], include: [ { model: Category, as: 'Category', attributes: ['name'] - }, { + }, + { model: User, - attributes: ['firstName', 'lastName', 'profileImage'], - }], - + attributes: ['firstName', 'lastName', 'profileImage'] + } + ] }); result.status = 200; @@ -129,17 +140,29 @@ export default class ArticleController { static async getArticle(req, res) { const { slug } = req.params; const article = await Article.findOne({ - attributes: ['id', 'title', 'description', 'body', 'slug', 'coverImage', 'tagList', 'createdAt', 'updatedAt'], + attributes: [ + 'id', + 'title', + 'description', + 'body', + 'slug', + 'coverImage', + 'tagList', + 'createdAt', + 'updatedAt' + ], where: { slug }, include: [ { model: Category, as: 'Category', attributes: ['name'] - }, { + }, + { model: User, - attributes: ['firstName', 'lastName', 'profileImage'], - }] + attributes: ['firstName', 'lastName', 'profileImage'] + } + ] }); if (article) { @@ -225,13 +248,15 @@ export default class ArticleController { } }); - return response[0]._options.isNewRecord === false ? errorSender(statusCodes.BAD_REQUEST, - res, - 'Message', - `Sorry, You can not report this ${slug} with the same reason twice, Thanks `) : res.status(statusCodes.CREATED).json({ - status: statusCodes.CREATED, - message: `Report for ${slug} is successfully submitted, Thanks` - }); + return response[0]._options.isNewRecord === false + ? errorSender(statusCodes.BAD_REQUEST, + res, + 'Message', + `Sorry, You can not report this ${slug} with the same reason twice, Thanks `) + : res.status(statusCodes.CREATED).json({ + status: statusCodes.CREATED, + message: `Report for ${slug} is successfully submitted, Thanks` + }); } catch (SequelizeForeignKeyConstraintError) { errorSender(statusCodes.NOT_FOUND, res, diff --git a/src/api/migrations/20190707124518-add-isBlocked.js b/src/api/migrations/20190707124518-add-isBlocked.js new file mode 100644 index 00000000..548a7ccc --- /dev/null +++ b/src/api/migrations/20190707124518-add-isBlocked.js @@ -0,0 +1,8 @@ +export default { + up: (queryInterface, Sequelize) => queryInterface.addColumn('Articles', 'isBlocked', { + type: Sequelize.BOOLEAN, + defaultValue: false + }), + + down: queryInterface => queryInterface.removeColumn('Articles', 'isBlocked') +}; diff --git a/src/api/models/article.js b/src/api/models/article.js index a8f4f769..e2542dc1 100644 --- a/src/api/models/article.js +++ b/src/api/models/article.js @@ -32,6 +32,10 @@ export default (sequelize, DataTypes) => { allowNull: false, type: DataTypes.INTEGER, references: { model: 'Categories', key: 'id' } + }, + isBlocked: { + type: DataTypes.BOOLEAN, + defaultValue: false } }, { diff --git a/src/api/models/reasons.js b/src/api/models/reasons.js index 6c1e314a..559e0481 100644 --- a/src/api/models/reasons.js +++ b/src/api/models/reasons.js @@ -16,6 +16,7 @@ export default (sequelize, DataTypes) => { Reasons.associate = ({ Report }) => { Reasons.hasMany(Report, { foreignKey: 'reasonId', + sourceKey: 'id', onDelete: 'cascade' }); }; diff --git a/src/api/models/report.js b/src/api/models/report.js index 06da458e..392fe94a 100644 --- a/src/api/models/report.js +++ b/src/api/models/report.js @@ -35,9 +35,9 @@ export default (sequelize, DataTypes) => { }); Report.removeAttribute('id'); Report.associate = ({ User, Article, Reason }) => { - Report.belongsTo(User, { foreignKey: 'userId' }); - Report.belongsTo(Article, { foreignKey: 'reportedArticleSlug' }); - Report.belongsTo(Reason, { foreignKey: 'reasonId' }); + Report.belongsTo(User, { foreignKey: 'userId', targetKey: 'id' }); + Report.belongsTo(Article, { foreignKey: 'reportedArticleSlug', targetKey: 'slug' }); + Report.belongsTo(Reason, { foreignKey: 'reasonId', targetKey: 'id' }); }; return Report; }; diff --git a/src/api/models/user.js b/src/api/models/user.js index 203dfb2f..91a2f676 100644 --- a/src/api/models/user.js +++ b/src/api/models/user.js @@ -68,12 +68,13 @@ export default (sequelize, DataTypes) => { onDelete: 'CASCADE', hooks: true }); + User.hasMany(models.Rating, { foreignKey: 'userId' }); User.hasMany(models.Highlight, { foreignKey: 'userId', - onDelete: 'CASCADE', + onDelete: 'CASCADE' }); User.hasMany(models.Following, { foreignKey: 'follower' @@ -81,11 +82,18 @@ export default (sequelize, DataTypes) => { User.hasMany(models.Following, { foreignKey: 'following' }); + + User.hasMany(models.Report, { + foreignKey: 'userId', + sourceId: 'id', + onDelete: 'CASCADE', + hooks: true + }); }; User.findByEmail = (email) => { const queryResult = User.findOne({ where: { email } }); - + return queryResult; }; return User; diff --git a/src/api/routes/adminRouter.js b/src/api/routes/adminRouter.js index 24c616cd..2e77150b 100644 --- a/src/api/routes/adminRouter.js +++ b/src/api/routes/adminRouter.js @@ -6,6 +6,10 @@ import validateToken from '../../middlewares/checkValidToken'; import checkUser from '../../middlewares/checkUser'; import checkIfAdmin from '../../middlewares/IsAdmin'; import checkIfBlocked from '../../middlewares/isBlocked'; +import articleController from '../controllers/articleController'; +import checkIfArticleExist from '../../middlewares/getOneArticle'; +import CheckIfModerator from '../../middlewares/CheckIfModerator'; +import CheckIfBlocked from '../../middlewares/isArticleBlocked'; const adminRouter = new Router(); @@ -54,4 +58,38 @@ adminRouter.patch('/users/:email/unblock', checkIfBlocked.checkUnBlocked, adminPermissionsController.unBlockUser); +adminRouter.get('/articles', + validateToken, + CheckIfModerator.CheckAdmins, + articleController.getArticles); + +adminRouter.get('/article/:slug', + validateToken, + CheckIfModerator.CheckAdmins, + articleController.getArticle); + +adminRouter.get('/articles/reported', + validateToken, + CheckIfModerator.CheckAdmins, + adminPermissionsController.getReportedArticles); + +adminRouter.get('/article/:slug/reported', + validateToken, + CheckIfModerator.CheckAdmins, + adminPermissionsController.getReportedArticle); + +adminRouter.patch('/article/:slug/block', + validateToken, + CheckIfModerator.CheckAdmins, + checkIfArticleExist.getArticle, + CheckIfBlocked.checkBlocked, + adminPermissionsController.blockArticle); + +adminRouter.patch('/article/:slug/unblock', + validateToken, + CheckIfModerator.CheckAdmins, + checkIfArticleExist.getArticle, + CheckIfBlocked.checkUnBlocked, + adminPermissionsController.unBlockArticle); + export default adminRouter; diff --git a/src/api/routes/index.js b/src/api/routes/index.js index c3336737..b5043e94 100644 --- a/src/api/routes/index.js +++ b/src/api/routes/index.js @@ -12,7 +12,6 @@ router.use('/articles', articleRouter); router.use('/users', authRouter); router.use('/authors', authorsRouter); router.use('/profiles', profileRouter); - router.use('/admin', adminRouter); export default router; diff --git a/src/api/seeders/20190621184055-user.js b/src/api/seeders/20190621184055-user.js index a6475e0a..1dd6f1e4 100644 --- a/src/api/seeders/20190621184055-user.js +++ b/src/api/seeders/20190621184055-user.js @@ -100,6 +100,7 @@ module.exports = { { username: 'BurindiAlain13', email: 'alain13@gmail.com', + role: 'moderator', password: hashedPassword, createdAt: new Date(), updatedAt: new Date() diff --git a/src/api/seeders/20190708175903-report.js b/src/api/seeders/20190708175903-report.js new file mode 100644 index 00000000..301e8860 --- /dev/null +++ b/src/api/seeders/20190708175903-report.js @@ -0,0 +1,22 @@ +module.exports = { + up: queryInterface => queryInterface.bulkInsert('Reports', + [ + { + userId: 12, + reportedArticleSlug: 'How-to-create-sequalize-seeds', + reasonId: 2, + createdAt: new Date(), + updatedAt: new Date() + }, + { + userId: 13, + reportedArticleSlug: 'How-to-create-sequalize-seedss', + reasonId: 1, + createdAt: new Date(), + updatedAt: new Date() + } + ], + {}), + + down: queryInterface => queryInterface.bulkDelete('Reports', null, {}) +}; diff --git a/src/helpers/constants/error.messages.js b/src/helpers/constants/error.messages.js index f2c71ae2..0e0e8536 100644 --- a/src/helpers/constants/error.messages.js +++ b/src/helpers/constants/error.messages.js @@ -30,7 +30,7 @@ export default { ratingsNotFound: 'No ratings found for this article', notOwner: 'Please you must be the owner of this Article in order to modify it, Thanks', noArticles: 'Article not found!', - noMoreArticle: 'There no more articles at the moment.', + noMoreArticle: 'There no more articles at the moment.', reason: 'a reason can not be a string, Please provide a number starting from 1, Thanks', textMatch: 'The text does not match the indexes', authorization: 'Please log in to perform this task', @@ -43,5 +43,6 @@ export default { blocked: 'is already blocked', notBlocked: 'is not blocked', noMoreUsers: 'There are no more users at the moment.', - exist: ' The user already exists' + exist: ' The user already exists', + noAccess: 'you must be a Moderator or an Admin to perform this operation' }; diff --git a/src/middlewares/CheckIfModerator.js b/src/middlewares/CheckIfModerator.js new file mode 100644 index 00000000..4bdf3f03 --- /dev/null +++ b/src/middlewares/CheckIfModerator.js @@ -0,0 +1,46 @@ +import Sequelize from 'sequelize'; +import models from '../api/models'; +import status from '../helpers/constants/status.codes'; +import sendError from '../helpers/error.sender'; + +const { User } = models; +/** + * check if user is admin + * + * @export + * @class CheckIfAdmin + */ +class CheckIfAdmin { + /** + * this is a middleware which checks if the user is admin. + * + * @static + * @param {object} req the request + * @param {object} res the response + * @param { object } next the next route controller to be called + * @memberof CheckUser + * @returns {Object} res + */ + static async CheckAdmins(req, res, next) { + const { + user: { email } + } = req.user; + const user = await User.findOne({ + where: { + email, + role: { + [Sequelize.Op.or]: ['moderator', 'admin'] + } + } + }); + if (user) { + return next(); + } + sendError(status.ACCESS_DENIED, + res, + 'role', + 'you must be a Moderator or an Admin to perform this operation'); + } +} + +export default CheckIfAdmin; diff --git a/src/middlewares/isArticleBlocked.js b/src/middlewares/isArticleBlocked.js new file mode 100644 index 00000000..4544a427 --- /dev/null +++ b/src/middlewares/isArticleBlocked.js @@ -0,0 +1,70 @@ +import models from '../api/models'; +import status from '../helpers/constants/status.codes'; +import sendError from '../helpers/error.sender'; + +const { Article } = models; +/** + * check if Article is blocked + * + * @export + * @class checkIfBlocked + */ +class checkIfBlocked { + /** + * this is a middleware which checks if a Article is blocked. + * + * @static + * @param {object} req the request + * @param {object} res the response + * @param { object } next the next route controller to be called + * @memberof checkArticle + * @returns {Object} res + */ + static async checkBlocked(req, res, next) { + const { slug } = req.params; + + const result = await Article.findOne({ + where: { + slug, + isBlocked: false + } + }); + + if (result) { + req.title = result.dataValues.title; + req.body = result.dataValues.body; + return next(); + } + sendError(status.BAD_REQUEST, res, 'isBlocked', `${slug} is already blocked`); + } + + /** + * this is a middleware which checks if a Article is not blocked. + * + * @static + * @param {object} req the request + * @param {object} res the response + * @param { object } next the next route controller to be called + * @memberof checkArticle + * @returns {Object} res + */ + static async checkUnBlocked(req, res, next) { + const { slug } = req.params; + + const result = await Article.findOne({ + where: { + slug, + isBlocked: true + } + }); + + if (result) { + req.title = result.dataValues.title; + req.body = result.dataValues.body; + return next(); + } + sendError(status.BAD_REQUEST, res, 'isBlocked', `${slug} is not blocked`); + } +} + +export default checkIfBlocked; diff --git a/tests/admin.article.test.js b/tests/admin.article.test.js new file mode 100644 index 00000000..04abdd06 --- /dev/null +++ b/tests/admin.article.test.js @@ -0,0 +1,179 @@ +import chai from 'chai'; +import '@babel/polyfill'; +import chaiHttp from 'chai-http'; +import status from '../src/helpers/constants/status.codes'; +import server from '../src/index'; +import error from '../src/helpers/constants/error.messages'; +import signup from '../src/helpers/tests/signup'; + +chai.use(chaiHttp); +chai.should(); + +const fakeAdmin = signup(); + +let userToken = ''; +const slug = 'How-to-create-sequalize-seeds'; +const password = 'password23423'; +const adminUrl = '/api/admin/article'; +const data = { + ReportedOn: '2019-07-11T07:58:55.653Z', + Article: { + title: 'How to create sequalize seeds', + slug: 'How-to-create-sequalize-seeds', + description: 'How to set dummy data automatically', + body: + "Suppose we want to insert some data into a few tables by default. If we follow up on previous example we can consider creating a demo user for User table.To manage all data migrations you can use seeders. Seed files are some change in data that can be used to populate database table with sample data or test data.Let's create a seed file which will add a demo user to our User table.", + coverImage: 'default.jpeg' + }, + User: { username: 'BurindiAlain12', email: 'alain12@gmail.com' }, + Reason: { description: 'this content exploits the minors' } +}; +describe('Article', () => { + it('should login the moderetor to get the token', (done) => { + chai + .request(server) + .post('/api/users/login') + .send({ + email: 'alain13@gmail.com', + password + }) + .end((err, res) => { + res.should.have.status(status.OK); + userToken = res.body.user.token; + done(); + }); + }); + it('Admin should fetch all articles', (done) => { + chai + .request(server) + .get(`${adminUrl}s`) + .set('Authorization', `${userToken}`) + .end((err, res) => { + res.should.have.status(status.OK); + res.body.result.Articles[0].title.should.include(data.Article.title); + res.body.result.Articles[0].description.should.include(data.Article.description); + res.body.result.Articles[0].body.should.include(data.Article.body); + done(); + }); + }); + it('Should respond with a message if there no more Articles', (done) => { + chai + .request(server) + .get(`${adminUrl}s/?offset=202&limit=10`) + .set('Authorization', `${userToken}`) + .end((err, res) => { + res.should.have.a.status(status.NOT_FOUND); + res.body.errors.should.have.property('Articles').eql(error.noMoreArticle); + done(); + }); + }); + it('Admin should fetch single article', (done) => { + chai + .request(server) + .get(`${adminUrl}/${slug}`) + .set('Authorization', `${userToken}`) + .end((err, res) => { + res.body.should.have.status(status.OK); + res.body.article.title.should.include(data.Article.title); + res.body.article.description.should.include(data.Article.description); + res.body.article.body.should.include(data.Article.body); + done(); + }); + }); + it('Admin should fetch all reported article', (done) => { + chai + .request(server) + .get(`${adminUrl}s/reported`) + .set('Authorization', `${userToken}`) + .end((err, res) => { + res.should.have.status(status.OK); + res.body.data[0].Reason.should.include(data.Reason); + res.body.data[0].User.should.include(data.User); + res.body.data[0].Article.title.should.include(data.Article.title); + res.body.data[0].Article.description.should.include(data.Article.description); + res.body.data[0].Article.body.should.include(data.Article.body); + done(); + }); + }); + it('Admin should fetch single reported article', (done) => { + chai + .request(server) + .get(`${adminUrl}/${slug}/reported`) + .set('Authorization', `${userToken}`) + .end((err, res) => { + res.should.have.status(status.OK); + res.body.data.Reason.should.include(data.Reason); + res.body.data.User.should.include(data.User); + res.body.data.Article.title.should.include(data.Article.title); + res.body.data.Article.description.should.include(data.Article.description); + res.body.data.Article.body.should.include(data.Article.body); + done(); + }); + }); + + it('Admin should block reported article', (done) => { + chai + .request(server) + .patch(`${adminUrl}/${slug}/block`) + .set('Authorization', `${userToken}`) + .end((err, res) => { + res.should.have.status(status.OK); + res.body.should.have.property('message').eql(`${slug} blocked successfully`); + done(); + }); + }); + it('Admin should not block reported article whitch is already blocked', (done) => { + chai + .request(server) + .patch(`${adminUrl}/${slug}/block`) + .set('Authorization', `${userToken}`) + .end((err, res) => { + res.should.have.status(status.BAD_REQUEST); + res.body.should.have + .property('errors') + .property('isBlocked') + .eql(`${slug} is already blocked`); + done(); + }); + }); + + it('Admin should unblock a blocked article', (done) => { + chai + .request(server) + .patch(`${adminUrl}/${slug}/unblock`) + .set('Authorization', `${userToken}`) + .end((err, res) => { + res.should.have.status(status.OK); + res.body.should.have.property('message').eql(`${slug} unblocked successfully`); + done(); + }); + }); + it('Admin should not unblock reported article whitch is not blocked', (done) => { + chai + .request(server) + .patch(`${adminUrl}/${slug}/unblock`) + .set('Authorization', `${userToken}`) + .end((err, res) => { + res.should.have.status(status.BAD_REQUEST); + res.body.should.have + .property('errors') + .property('isBlocked') + .eql(`${slug} is not blocked`); + done(); + }); + }); + it('Admin should fail to unblock an Article because the unblocker is not admin', (done) => { + chai + .request(server) + .patch(`${adminUrl}/${slug}/unblock`) + .set('Authorization', `${fakeAdmin}`) + .end((err, res) => { + res.should.have.status(status.ACCESS_DENIED); + res.body.should.have + .property('errors') + .property('role') + .eql(error.noAccess); + done(); + }); + }); +});