From 096e6422aaf1ada3defe636171896acca2875fa2 Mon Sep 17 00:00:00 2001 From: Ibidapo Rasheed Date: Wed, 20 Feb 2019 19:25:02 +0100 Subject: [PATCH 1/2] feature(articleSearch/Filter): Implement search/filter article functionality - Implement filter by author username only - Implement filter by tag only - Implement search by keywords - Implement any combination of supplied filters [Delivers #163478809] --- package-lock.json | 5 ++ package.json | 2 + server/api/controllers/article.js | 23 +++++- server/api/controllers/comments.js | 121 +++++++++++++++++++++++++---- server/api/controllers/user.js | 29 ++++--- tests/article/article.js | 10 ++- tests/auth/social-login.js | 22 ++++-- 7 files changed, 169 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index 17cde37..0541dc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5318,6 +5318,11 @@ } } }, + "loadash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/loadash/-/loadash-1.0.0.tgz", + "integrity": "sha512-xlX5HBsXB3KG0FJbJJG/3kYWCfsCyCSus3T+uHVu6QL6YxAdggmm3QeyLgn54N2yi5/UE6xxL5ZWJAAiHzHYEg==" + }, "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", diff --git a/package.json b/package.json index 9819475..2f42e1a 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "fs": "0.0.1-security", "joi": "^14.3.1", "jsonwebtoken": "^8.3.0", + "loadash": "^1.0.0", + "lodash": "^4.17.11", "methods": "^1.1.2", "mongoose": "^5.2.2", "mongoose-unique-validator": "^2.0.1", diff --git a/server/api/controllers/article.js b/server/api/controllers/article.js index af607f5..3e7ce34 100644 --- a/server/api/controllers/article.js +++ b/server/api/controllers/article.js @@ -1,8 +1,9 @@ import Sequelize from 'sequelize'; import { createLogger, format, transports } from 'winston'; +import { omit } from 'lodash'; import notifyFollowers from '../helpers/notification/followers'; import { - User, Article, LikeDislike, Tag, Rating, Category + User, Article, LikeDislike, Tag, Rating, Category, Comment } from '../../models'; import { getArticlesByAllParams, getArticlesBySearchTagParams, getArticlesBySearchAuthorParams, @@ -529,9 +530,29 @@ export const getArticle = async (req, res) => { message: 'Resource not found' }); } + const comments = await Comment.findAll({ + where: { articleId: { [Op.eq]: foundArticle.id } }, + order: [['updatedAt', 'DESC']] + }); + + let articleComments = comments.map(async (comment) => { + const user = await User.findById(comment.userId, { + attributes: { + exclude: ['id', 'username', 'email', 'password', + 'role', 'bio', 'imageUrl', 'verified', 'createdAt', 'updatedAt'] + }, + }); + + const editedComment = omit(comment.toJSON(), ['userId']); + editedComment.user = user.toJSON(); + return editedComment; + }); + + articleComments = await Promise.all(articleComments); foundArticle = foundArticle.toJSON(); foundArticle.readTime = getReadTime(foundArticle.body); + foundArticle.comments = articleComments; return res.status(200).send({ status: 'success', diff --git a/server/api/controllers/comments.js b/server/api/controllers/comments.js index f44b7e7..ee4d1a0 100644 --- a/server/api/controllers/comments.js +++ b/server/api/controllers/comments.js @@ -1,18 +1,68 @@ +import { omit } from 'lodash'; import { - Comment, Article, LikeComment, Sequelize + Comment, + User, + Article, + LikeComment, + Sequelize } from '../../models'; import informBookmarkers from '../helpers/notification/bookmarkers'; -const { Op } = Sequelize; +const { + Op +} = Sequelize; export const postComment = async (req, res) => { - const { params: { articleId }, body: { commentBody }, user: { id: userId } } = req; + const { + params: { + articleId + }, + body: { + commentBody + }, + user: { + id: userId + } + } = req; try { - const newComment = await Comment.create({ commentBody, articleId, userId }); - + let newComment = await Comment.create({ + commentBody, + articleId, + userId + }); informBookmarkers(articleId, commentBody, userId); - return res.status(201).send({ status: 'success', data: newComment }); + + const comments = await Comment.findAll({ + where: { + articleId: { + [Op.eq]: articleId + } + }, + order: [['updatedAt', 'DESC']] + }); + + let allComments = comments.map(async (comment) => { + const user = await User.findById(comment.userId, { + attributes: { + exclude: ['id', 'username', 'email', 'password', + 'role', 'bio', 'imageUrl', 'verified', 'createdAt', 'updatedAt'] + }, + }); + + const editedComment = omit(comment.toJSON(), ['userId']); + editedComment.user = user.toJSON(); + return editedComment; + }); + + allComments = await Promise.all(allComments); + newComment = newComment.toJSON(); + newComment.comments = allComments; + + return res.status(201).send({ + status: 'success', + data: newComment.comments + }); } catch (error) { return res.status(500).send({ status: 'error', @@ -23,7 +73,15 @@ export const postComment = async (req, res) => { }; export const updateComment = async (req, res) => { - const { params: { articleId, commentId }, body: { commentBody } } = req; + const { + params: { + articleId, + commentId + }, + body: { + commentBody + } + } = req; try { const foundArticle = await Article.findByPk(articleId); if (!foundArticle) { @@ -32,7 +90,13 @@ export const updateComment = async (req, res) => { message: 'Article not found' }); } - const foundComments = await foundArticle.getComments({ where: { id: { [Op.eq]: commentId } } }); + const foundComments = await foundArticle.getComments({ + where: { + id: { + [Op.eq]: commentId + } + } + }); const foundComment = foundComments[0]; if (!foundComment) { @@ -49,8 +113,12 @@ export const updateComment = async (req, res) => { }); // update the comment - const updatedComment = await foundComment.update({ commentBody }, - { returning: true, plain: true }); + const updatedComment = await foundComment.update({ + commentBody + }, { + returning: true, + plain: true + }); const oldComments = await updatedComment.getCommentHistories(); const comment = updatedComment.toJSON(); @@ -78,11 +146,24 @@ export const updateComment = async (req, res) => { */ export const deleteComment = async (req, res) => { try { - const { params: { articleId, commentId }, user: { id: userId, role } } = req; + const { + params: { + articleId, + commentId + }, + user: { + id: userId, + role + } + } = req; const foundComment = await Comment.findOne({ where: { - id: { [Op.eq]: commentId }, - articleId: { [Op.eq]: articleId }, + id: { + [Op.eq]: commentId + }, + articleId: { + [Op.eq]: articleId + }, } }); @@ -130,10 +211,20 @@ export const deleteComment = async (req, res) => { */ export const likeComment = async (req, res) => { try { - const { params: { commentId }, user: { id: userId } } = req; + const { + params: { + commentId + }, + user: { + id: userId + } + } = req; await LikeComment.findOrCreate({ - where: { commentId, userId } + where: { + commentId, + userId + } }).spread((like, created) => { if (created) { return res.status(201).send({ diff --git a/server/api/controllers/user.js b/server/api/controllers/user.js index 4cdb9da..ea7a8b0 100644 --- a/server/api/controllers/user.js +++ b/server/api/controllers/user.js @@ -132,7 +132,7 @@ export const socialLogin = async (req, res) => { firstname, lastname, username, email, password, imageUrl } = req.user; try { - const [{ id, role }, isNew] = await User.findOrCreate({ + const [{ id, role, isVerified }] = await User.findOrCreate({ where: { email }, defaults: { firstname, lastname, username, password, imageUrl @@ -140,15 +140,15 @@ export const socialLogin = async (req, res) => { }); let token; - const paramToken = isNew && signToken({ email }, '10m'); + const paramToken = isVerified && signToken({ email }, '10m'); - if (process.env.NODE_ENV === 'production' && isNew) { + if (process.env.NODE_ENV === 'production' && !isVerified) { try { await resetPasswordVerificationMail(username, email, paramToken); } catch (error) { logger.debug('Email Error::', error); } - } else if (!isNew) { + } else if (!isVerified) { token = signToken({ sid: req.sessionID, id, @@ -157,18 +157,15 @@ export const socialLogin = async (req, res) => { }); } - return res.status(200).send({ - status: 'success', - data: { - message: `${isNew ? `An email has been sent to ${email}. Follow contained instructions to verify your account and create password.` : 'Login Successful.'}`, - ...( - isNew ? { link: `${req.protocol}://${req.headers.host}/api/users/resetPassword/${paramToken}` } - : { - token, username, email, role - } - ) - } - }); + const verifiedUrl = `dashboard#token${token}`; + + return res.redirect( + `https://denethor-ah-frontend-staging.herokuapp.com/${ + isVerified + ? verifiedUrl + : 'signup?mailalert=true' + }` + ); } catch (e) { return res.status(500).send({ status: 'fail', diff --git a/tests/article/article.js b/tests/article/article.js index a0044b1..521dbfc 100644 --- a/tests/article/article.js +++ b/tests/article/article.js @@ -166,7 +166,7 @@ describe('Tests for article resource', () => { userToken4 = newToken; - const { body: { data: { id: newCommentId } } } = await chai.request(app) + const { body: { data: [{ id: newCommentId }] } } = await chai.request(app) .post(`/api/articles/${articleId}/comments`) .set('Authorization', `Bearer ${userToken}`) .send({ @@ -182,7 +182,7 @@ describe('Tests for article resource', () => { .set('Authorization', `Bearer ${userToken}`) .send(comment); const { - body: { data: { id: newCommentId, commentBody, articleId: returnedArticleId } } + body: { data: [{ id: newCommentId, commentBody, articleId: returnedArticleId }] } } = res; commentId = newCommentId; expect(res).to.have.status(201); @@ -768,6 +768,10 @@ describe('Tests for article resource', () => { .set('Authorization', `Bearer ${userToken}`) .send(mockArticle); articleId = res.body.data.id; + await chai.request(app) + .post(`/api/articles/${articleId}/comments`) + .set('Authorization', `Bearer ${userToken}`) + .send(comment); }); it('should return all articles', async () => { @@ -837,7 +841,7 @@ describe('Tests for article resource', () => { .post(`/api/articles/${articleId}/comments`) .set('Authorization', `Bearer ${userToken}`) .send(comment); - commentId = res.body.data.id; + commentId = res.body.data[0].id; }); it('should update a comment', async () => { diff --git a/tests/auth/social-login.js b/tests/auth/social-login.js index b464ae4..6032543 100755 --- a/tests/auth/social-login.js +++ b/tests/auth/social-login.js @@ -1,10 +1,16 @@ -import chai, { expect } from 'chai'; +import chai, { + expect +} from 'chai'; import passport from 'passport'; import sinon from 'sinon'; import chaiHttp from 'chai-http'; import app from '../../index'; -import models, { sequelize } from '../../server/models'; -import { mockStrategy } from '../mocks/mockStrategy'; +import models, { + sequelize +} from '../../server/models'; +import { + mockStrategy +} from '../mocks/mockStrategy'; import mockRoles from '../mocks/mockRoles'; chai.use(chaiHttp); @@ -18,7 +24,10 @@ describe('Test Cases for the social login endpoints', () => { after((done) => { Object.values(sequelize.models).map(function (model) { - return model.destroy({ where: {}, force: true }); + return model.destroy({ + where: {}, + force: true + }); }); sequelize.queryInterface.sequelize.query('TRUNCATE TABLE session CASCADE;').then(() => done()); }); @@ -26,9 +35,6 @@ describe('Test Cases for the social login endpoints', () => { it('Should create account for user once platform returns payload', async () => { const res = await chai.request(app) .get('/api/users/google/redirect'); - const { body: { status, data } } = res; - expect(res).to.have.status(200); - expect(data).to.have.property('link'); - expect(status).to.eql('success'); + expect(res).to.redirectTo('https://denethor-ah-frontend-staging.herokuapp.com/signup?mailalert=true'); }); }); From 71259df6e54540dbc83f96dc0e9934ca27cf3a7a Mon Sep 17 00:00:00 2001 From: Omoefe Dukuye Date: Wed, 13 Mar 2019 15:06:21 +0100 Subject: [PATCH 2/2] Add frontend redirect for social login --- server/api/controllers/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/api/controllers/user.js b/server/api/controllers/user.js index ea7a8b0..3270a81 100644 --- a/server/api/controllers/user.js +++ b/server/api/controllers/user.js @@ -160,7 +160,7 @@ export const socialLogin = async (req, res) => { const verifiedUrl = `dashboard#token${token}`; return res.redirect( - `https://denethor-ah-frontend-staging.herokuapp.com/${ + `${process.env.REACT_ENDPOINT}/${ isVerified ? verifiedUrl : 'signup?mailalert=true'