From c7d2a9a94f2d5322fa02c2c7e7e8cd15d2e6c94b Mon Sep 17 00:00:00 2001 From: David Jay Date: Thu, 17 Nov 2016 14:52:08 -0500 Subject: [PATCH 01/19] Adding padding for notifications at the bottom of the comment stream. --- client/coral-embed-stream/style/default.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 2f7394d4d..86fc66026 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -3,7 +3,9 @@ body { font-family: 'Open Sans', sans-serif; width: 100%; font-size: 12px; - margin: 0; + margin: 0px; + padding: 0px 0px 50px 0px; + } button { From 243c36a306f37a13f259848800c565f013db99f9 Mon Sep 17 00:00:00 2001 From: David Jay Date: Thu, 17 Nov 2016 17:45:44 -0500 Subject: [PATCH 02/19] Switching Permalink props on replies to avoid null errors. --- client/coral-embed-stream/src/CommentStream.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index ecfc6d7d2..1ed51a5d9 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -187,8 +187,8 @@ class CommentStream extends Component { updateItem={this.props.updateItem} currentUser={this.props.auth.user}/> ; From 53fa63b269c9134ba93cbea1df87a39f30ccea32 Mon Sep 17 00:00:00 2001 From: David Jay Date: Fri, 18 Nov 2016 11:30:38 -0500 Subject: [PATCH 03/19] Fixing bug where reply buttons in replies to not operate as expected. --- client/coral-embed-stream/src/CommentStream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index 1ed51a5d9..72e4e5919 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -165,7 +165,7 @@ class CommentStream extends Component {
+ parent_id={reply.parent_id}/> Date: Fri, 18 Nov 2016 11:50:12 -0500 Subject: [PATCH 04/19] Addressing but where flag increases like count. --- models/action.js | 12 +++++------- tests/models/action.js | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/models/action.js b/models/action.js index 574f3439f..43e38795c 100644 --- a/models/action.js +++ b/models/action.js @@ -45,25 +45,23 @@ ActionSchema.statics.getActionSummaries = function(item_ids) { return ActionSchema.statics.findByItemIdArray(item_ids).then((rawActions) => { // Create an object with a count of each action type for each item const actionSummaries = rawActions.reduce((actionObj, action) => { - if (!actionObj[action.item_id]) { - actionObj[action.item_id] = { + if (!actionObj[`${action.item_id}_${action.action_type}`]) { + actionObj[`${action.item_id}_${action.action_type}`] = { id: action.id, + item_id: action.item_id, item_type: action.item_type, action_type: action.action_type, count: 1, current_user: false //Update this later when we have authentication }; } else { - actionObj[action.item_id].count ++; + actionObj[`${action.item_id}_${action.action_type}`].count ++; } return actionObj; }, {}); - // Return an array extracted from the actionSummaries object return Object.keys(actionSummaries).reduce((actions, key) => { - let actionSummary = actionSummaries[key]; - actionSummary.item_id = key; - actions.push(actionSummary); + actions.push(actionSummaries[key]); return actions; }, []); }); diff --git a/tests/models/action.js b/tests/models/action.js index 257c191d6..917191690 100644 --- a/tests/models/action.js +++ b/tests/models/action.js @@ -22,6 +22,10 @@ describe('Action: models', () => { action_type: 'flag', item_id: '123', item_type: 'comments' + }, { + action_type: 'like', + item_id: '123', + item_type: 'comments' }]).then((actions) => { mockActions = actions; }); @@ -39,7 +43,7 @@ describe('Action: models', () => { describe('#findByItemIdArray()', () => { it('should find an array of actions from an array of item_ids', () => { return Action.findByItemIdArray(['123', '456']).then((result) => { - expect(result).to.have.length(3); + expect(result).to.have.length(4); }); }); }); @@ -47,10 +51,11 @@ describe('Action: models', () => { describe('#getActionSummaries()', () => { it('should return properly formatted summaries from an array of item_ids', () => { return Action.getActionSummaries(['123', '789']).then((result) => { - expect(result).to.have.length(2); + expect(result).to.have.length(3); const sorted = result.sort((a, b) => a.count - b.count); delete sorted[0].id; delete sorted[1].id; + delete sorted[2].id; expect(sorted[0]).to.deep.equal({ action_type: 'like', count: 1, @@ -59,6 +64,13 @@ describe('Action: models', () => { current_user: false }); expect(sorted[1]).to.deep.equal({ + action_type: 'like', + count: 1, + item_id: '123', + item_type: 'comments', + current_user: false + }); + expect(sorted[2]).to.deep.equal({ action_type: 'flag', count: 2, item_id: '123', From b14bd02de61d63efcd9f3bb351042972f02a7777 Mon Sep 17 00:00:00 2001 From: David Jay Date: Fri, 18 Nov 2016 11:51:08 -0500 Subject: [PATCH 05/19] Addressing style bug in FlagButton. --- client/coral-plugin-flags/FlagButton.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/FlagButton.js index a4869c486..1b1d114c6 100644 --- a/client/coral-plugin-flags/FlagButton.js +++ b/client/coral-plugin-flags/FlagButton.js @@ -31,7 +31,7 @@ const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, ad : {lang.t('flag')} } flag
; From fe4f8bc4646ec9a2c5b85ed33e1a4a9e5648e4f1 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 18 Nov 2016 09:56:52 -0700 Subject: [PATCH 06/19] Replaced query with aggregation --- models/action.js | 73 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/models/action.js b/models/action.js index 43e38795c..8173db9f9 100644 --- a/models/action.js +++ b/models/action.js @@ -42,29 +42,58 @@ ActionSchema.statics.findByItemIdArray = function(item_ids) { * @param {String} ids array of user identifiers (uuid) */ ActionSchema.statics.getActionSummaries = function(item_ids) { - return ActionSchema.statics.findByItemIdArray(item_ids).then((rawActions) => { - // Create an object with a count of each action type for each item - const actionSummaries = rawActions.reduce((actionObj, action) => { - if (!actionObj[`${action.item_id}_${action.action_type}`]) { - actionObj[`${action.item_id}_${action.action_type}`] = { - id: action.id, - item_id: action.item_id, - item_type: action.item_type, - action_type: action.action_type, - count: 1, - current_user: false //Update this later when we have authentication - }; - } else { - actionObj[`${action.item_id}_${action.action_type}`].count ++; + return Action.aggregate([ + { + + // only grab items that match the specified item id's + $match: { + item_id: { + $in: item_ids + } } - return actionObj; - }, {}); - // Return an array extracted from the actionSummaries object - return Object.keys(actionSummaries).reduce((actions, key) => { - actions.push(actionSummaries[key]); - return actions; - }, []); - }); + }, + { + $group: { + + // group unique documents by these properties, we are leveraging the + // fact that each uuid is completly unique. + _id: { + item_id: '$item_id', + action_type: '$action_type' + }, + + // and sum up all actions matching the above grouping criteria + count: { + $sum: 1 + }, + + // we are leveraging the fact that each uuid is completly unique and + // just grabbing the last instance of the item type here. + item_type: { + $last: '$item_type' + } + } + }, + { + $project: { + + // suppress the _id field + _id: false, + + // map the fields from the _id grouping down a level + item_id: '$_id.item_id', + action_type: '$_id.action_type', + + // map the field directly + count: '$count', + item_type: '$item_type', + + // set the current user to false here + current_user: {$literal: false} + } + } + ]) + .exec(); }; /* From 21d96f2210de78b6a77b327e651a975100fd1aa0 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 18 Nov 2016 10:12:22 -0700 Subject: [PATCH 07/19] Added fix for error view --- app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index 6512680aa..f5714fe6e 100644 --- a/app.js +++ b/app.js @@ -97,7 +97,7 @@ app.use('/api', (err, req, res, next) => { res.status(err.status || 500); res.json({ message: err.message, - error: app.get('env') === 'development' ? err : null + error: app.get('env') === 'development' ? err : {} }); }); @@ -109,7 +109,7 @@ app.use('/', (err, req, res, next) => { res.status(err.status || 500); res.render('error', { message: err.message, - error: app.get('env') === 'development' ? err : null + error: app.get('env') === 'development' ? err : {} }); }); From 88f963b56411195db9c42223c1c6c7a5bbf13dc0 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 18 Nov 2016 10:18:44 -0700 Subject: [PATCH 08/19] Removed unneeded lines in test --- tests/models/action.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/models/action.js b/tests/models/action.js index 917191690..80a8ae814 100644 --- a/tests/models/action.js +++ b/tests/models/action.js @@ -52,10 +52,9 @@ describe('Action: models', () => { it('should return properly formatted summaries from an array of item_ids', () => { return Action.getActionSummaries(['123', '789']).then((result) => { expect(result).to.have.length(3); + const sorted = result.sort((a, b) => a.count - b.count); - delete sorted[0].id; - delete sorted[1].id; - delete sorted[2].id; + expect(sorted[0]).to.deep.equal({ action_type: 'like', count: 1, @@ -63,6 +62,7 @@ describe('Action: models', () => { item_type: 'comments', current_user: false }); + expect(sorted[1]).to.deep.equal({ action_type: 'like', count: 1, @@ -70,6 +70,7 @@ describe('Action: models', () => { item_type: 'comments', current_user: false }); + expect(sorted[2]).to.deep.equal({ action_type: 'flag', count: 2, From da07b737b2d27750e02bbcc7d9a47b1c44cbb16b Mon Sep 17 00:00:00 2001 From: David Jay Date: Fri, 18 Nov 2016 12:19:58 -0500 Subject: [PATCH 09/19] Allowing commentstream to be rendered outside of embed. --- client/coral-embed-stream/src/CommentStream.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index 6fc48a826..f5ac55db8 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -61,8 +61,8 @@ class CommentStream extends Component { // Set up messaging between embedded Iframe an parent component // Using recommended Pym init code which violates .eslint standards const pym = new Pym.Child({polling: 100}); - const path = /https?\:\/\/([^?]+)/.exec(pym.parentUrl)[1]; - this.props.getStream(path); + const path = /https?\:\/\/([^?]+)/.exec(pym.parentUrl); + this.props.getStream(path && path[1] || window.location); } render () { From a0db48ea224059905ab79c27980346d384c3817c Mon Sep 17 00:00:00 2001 From: David Jay Date: Fri, 18 Nov 2016 13:10:59 -0500 Subject: [PATCH 10/19] Adding author id to posted comments. --- client/coral-embed-stream/src/CommentStream.js | 2 +- client/coral-plugin-commentbox/CommentBox.js | 11 ++++++----- swagger.yaml | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index cb5fcd89a..7d8669d38 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -105,7 +105,7 @@ class CommentStream extends Component { id={rootItemId} premod={this.props.config.moderation} reply={false} - canPost={loggedIn} + authorId={user.id} /> {!loggedIn && } diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index e712d8e24..be03177f8 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -12,7 +12,8 @@ class CommentBox extends Component { id: PropTypes.string, comments: PropTypes.array, reply: PropTypes.bool, - canPost: PropTypes.bool + canPost: PropTypes.bool, + currentUser: PropTypes.object } state = { @@ -21,11 +22,11 @@ class CommentBox extends Component { } postComment = () => { - const {postItem, updateItem, id, parent_id, addNotification, appendItemArray, premod} = this.props; + const {postItem, updateItem, id, parent_id, addNotification, appendItemArray, premod, authorId} = this.props; let comment = { body: this.state.body, asset_id: id, - username: this.state.username + user_id: authorId.id }; let related; let parent_type; @@ -52,7 +53,7 @@ class CommentBox extends Component { } render () { - const {styles, reply, canPost} = this.props; + const {styles, reply, authorId} = this.props; // How to handle language in plugins? Should we have a dependency on our central translation file? return
- { canPost && ( + { authorId && ( + { + passwordRequestSuccess + ?

{passwordRequestSuccess}

+ : null + } + { + passwordRequestFailure + ?

{passwordRequestFailure}

+ : null + } + +
+ {lang.t('signIn.needAnAccount')} changeView('SIGNUP')}>{lang.t('signIn.register')} + {lang.t('signIn.alreadyHaveAnAccount')} changeView('SIGNIN')}>{lang.t('signIn.signIn')} +
- - -
- {lang.t('signIn.needAnAccount')} changeView('SIGNUP')}>{lang.t('signIn.register')} - {lang.t('signIn.alreadyHaveAnAccount')} changeView('SIGNIN')}>{lang.t('signIn.signIn')} -
-
-); + ); + } +} export default ForgotContent; diff --git a/client/coral-sign-in/components/styles.css b/client/coral-sign-in/components/styles.css index e8458f314..1561f557b 100644 --- a/client/coral-sign-in/components/styles.css +++ b/client/coral-sign-in/components/styles.css @@ -129,3 +129,14 @@ input.error{ .action { margin-top: 15px; } + +.passwordRequestSuccess { + border: 1px solid green; + background-color: lightgreen; + padding: 10px; +} + +.passwordRequestFailure { + border: 1px solid orange; + background-color: 1px solid coral +} diff --git a/init.js b/init.js index 9c3c2bda5..768d02999 100644 --- a/init.js +++ b/init.js @@ -1,6 +1,15 @@ const Setting = require('./models/setting'); +const wordlist = require('./services/wordlist'); -const defaults = {id: '1', moderation: 'pre'}; -module.exports = Setting.init(defaults); +module.exports = () => Promise.all([ -// presumably this file will grow, which is why I've broken it out. + // Upsert the settings object. + Setting + .init({id: '1', moderation: 'pre'}) + .then(() => { + + // Load in the wordlist now that settings have been init'd. + return wordlist.init(); + }) + +]); diff --git a/models/action.js b/models/action.js index 8173db9f9..91378b0b0 100644 --- a/models/action.js +++ b/models/action.js @@ -117,9 +117,7 @@ ActionSchema.statics.findCommentsIdByActionType = function(action_type, item_typ return Action.find({ 'action_type': action_type, 'item_type': item_type - }, - 'item_id' - ); + }, 'item_id'); }; const Action = mongoose.model('Action', ActionSchema); diff --git a/models/comment.js b/models/comment.js index d17e98860..2ac130978 100644 --- a/models/comment.js +++ b/models/comment.js @@ -17,7 +17,6 @@ const CommentSchema = new Schema({ }, asset_id: String, author_id: String, - username: String, status: { type: String, enum: ['accepted', 'rejected', ''], @@ -31,19 +30,6 @@ const CommentSchema = new Schema({ } }); -//============================================================================== -// New Statics -//============================================================================== - -/** - * Create a comment. - * @param {String} body content of comment -*/ -CommentSchema.statics.new = function(body, author_id, asset_id, parent_id, status, username) { - let comment = new Comment({body, author_id, asset_id, parent_id, status, username}); - return comment.save(); -}; - //============================================================================== // Find Statics //============================================================================== @@ -51,7 +37,8 @@ CommentSchema.statics.new = function(body, author_id, asset_id, parent_id, statu /** * Finds a comment by the id. * @param {String} id identifier of comment (uuid) -*/ + * @return {Promise} + */ CommentSchema.statics.findById = function(id) { return Comment.findOne({'id': id}); }; @@ -59,7 +46,8 @@ CommentSchema.statics.findById = function(id) { /** * Finds ALL the comments by the asset_id. * @param {String} asset_id identifier of the asset which owns this comment (uuid) -*/ + * @return {Promise} + */ CommentSchema.statics.findByAssetId = function(asset_id) { return Comment.find({asset_id}); }; @@ -68,7 +56,8 @@ CommentSchema.statics.findByAssetId = function(asset_id) { * Finds the accepted comments by the asset_id. * get the comments that are accepted. * @param {String} asset_id identifier of the asset which owns the comments (uuid) -*/ + * @return {Promise} + */ CommentSchema.statics.findAcceptedByAssetId = function(asset_id) { return Comment.find({asset_id: asset_id, status:'accepted'}); }; @@ -76,7 +65,8 @@ CommentSchema.statics.findAcceptedByAssetId = function(asset_id) { /** * Finds the new and accepted comments by the asset_id. * @param {String} asset_id identifier of the asset which owns the comments (uuid) -*/ + * @return {Promise} + */ CommentSchema.statics.findAcceptedAndNewByAssetId = function(asset_id) { return Comment.find({asset_id: asset_id, status: {'$in': ['accepted', '']}}); }; @@ -84,7 +74,8 @@ CommentSchema.statics.findAcceptedAndNewByAssetId = function(asset_id) { /** * Find comments by an action that was performed on them. * @param {String} action_type the type of action that was performed on the comment -*/ + * @return {Promise} + */ CommentSchema.statics.findByActionType = function(action_type) { return Action .findCommentsIdByActionType(action_type, 'comment') @@ -99,50 +90,54 @@ CommentSchema.statics.findByActionType = function(action_type) { * Find not moderated comments by an action that was performed on them. * @param {String} action_type the type of action that was performed on the comment * @param {String} status the status of the comment to search for -*/ + * @return {Promise} + */ CommentSchema.statics.findByStatusByActionType = function(status, action_type) { return Action .findCommentsIdByActionType(action_type, 'comment') .then((actions) => { - return Comment.find({ - 'status': status, - 'id': { - '$in': actions.map(a => { - return a.item_id; - }) + status: status, + id: { + $in: actions.map(a => a.item_id) } }); - }); }; /** * Find comments by their status. * @param {String} status the status of the comment to search for -*/ + * @return {Promise} + */ CommentSchema.statics.findByStatus = function(status) { - return Comment.find({'status': status}); + return Comment.find({ + status: status === 'new' ? '' : status + }); }; /** * Find comments that need to be moderated (aka moderation queue). * @param {String} moderationValue pre or post moderation setting. If it is undefined then look at the settings. -*/ + * @return {Promise} + */ CommentSchema.statics.moderationQueue = function(moderation) { switch(moderation){ + // Pre-moderation: New comments are shown in the moderator queues immediately. case 'pre': return Comment.findByStatus('').then((comments) => { return comments; }); + // Post-moderation: New comments do not appear in moderation queues unless they are flagged by other users. case 'post': return Comment.findByStatusByActionType('', 'flag').then((comments) => { return comments; }); + default: - throw new Error('Moderation setting not found.'); + return Promise.reject(Error('Moderation setting not found.')); } }; @@ -154,16 +149,18 @@ CommentSchema.statics.moderationQueue = function(moderation) { * Change the status of a comment. * @param {String} id identifier of the comment (uuid) * @param {String} status the new status of the comment -*/ + * @return {Promise} + */ CommentSchema.statics.changeStatus = function(id, status) { - return Comment.findOneAndUpdate({'id': id}, {$set: {'status': status}}, {upsert: false, new: true}); + return Comment.findOneAndUpdate({'id': id}, {$set: {'status': status}}); }; /** * Add an action to the comment. * @param {String} id identifier of the comment (uuid) * @param {String} action the new action to the comment -*/ + * @return {Promise} + */ CommentSchema.statics.addAction = function(id, user_id, action_type) { // check that the comment exist let action = new Action({ @@ -183,7 +180,8 @@ CommentSchema.statics.addAction = function(id, user_id, action_type) { * Change the status of a comment. * @param {String} id identifier of the comment (uuid) * @param {String} status the new status of the comment -*/ + * @return {Promise} + */ CommentSchema.statics.removeById = function(id) { return Comment.remove({'id': id}); }; @@ -193,7 +191,8 @@ CommentSchema.statics.removeById = function(id) { * @param {String} id identifier of the comment (uuid) * @param {String} action_type the type of the action to be removed * @param {String} user_id the id of the user performing the action -*/ + * @return {Promise} + */ CommentSchema.statics.removeAction = function(item_id, user_id, action_type) { return Action.remove({ action_type, @@ -203,6 +202,15 @@ CommentSchema.statics.removeAction = function(item_id, user_id, action_type) { }); }; +/** + * Returns all the comments in the collection. + * @return {Promise} + */ +CommentSchema.statics.all = () => { + return Comment.find(); +}; + +// Comment model. const Comment = mongoose.model('Comment', CommentSchema); module.exports = Comment; diff --git a/models/setting.js b/models/setting.js index fb35e1eef..f0c6a1abc 100644 --- a/models/setting.js +++ b/models/setting.js @@ -2,7 +2,7 @@ const mongoose = require('../mongoose'); const Schema = mongoose.Schema; /** - * this Schema manages application settings that get used on front- and backend + * this Schema manages application settings that get used on front and backend * NOTE: when you set a setting here, it will not automatically be exposed to * the front end. You must add it to the whitelist in the settings route * in /routes/api/settings/index.js @@ -12,7 +12,8 @@ const SettingSchema = new Schema({ id: {type: String, default: '1'}, moderation: {type: String, enum: ['pre', 'post'], default: 'pre'}, infoBoxEnable: {type: Boolean, default: false}, - infoBoxContent: {type: String, default: ''} + infoBoxContent: {type: String, default: ''}, + wordlist: [String] }, { timestamps: { createdAt: 'created_at', diff --git a/package.json b/package.json index fb83055b6..9300c347f 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,8 @@ "config": { "pre-git": { "commit-msg": [], - "pre-commit": [ - "npm run lint", - "npm test" - ], - "pre-push": [ - "npm test" - ], + "pre-commit": ["npm run lint", "npm test"], + "pre-push": ["npm test"], "post-commit": [], "post-merge": [] } @@ -31,12 +26,7 @@ "type": "git", "url": "git+https://github.com/coralproject/talk.git" }, - "keywords": [ - "talk", - "coral", - "coralproject", - "ask" - ], + "keywords": ["talk", "coral", "coralproject", "ask"], "author": "", "license": "Apache-2.0", "bugs": { @@ -56,13 +46,13 @@ "helmet": "^3.1.0", "jsonwebtoken": "^7.1.9", "lodash": "^4.16.6", - "lodash.debounce": "^4.0.8", "mongoose": "^4.6.5", "morgan": "^1.7.0", "nodemailer": "^2.6.4", "passport": "^0.3.2", "passport-facebook": "^2.1.1", "passport-local": "^1.0.0", + "natural": "^0.4.0", "prompt": "^1.0.0", "react-linkify": "^0.1.3", "redis": "^2.6.3", diff --git a/routes/admin/index.js b/routes/admin/index.js index 2b967708a..9d3cbd0a9 100644 --- a/routes/admin/index.js +++ b/routes/admin/index.js @@ -5,9 +5,12 @@ router.get('/embed/stream/preview', (req, res) => { res.render('embed-stream', {basePath: '/client/embed/stream'}); }); -router.get('/password-reset/:token', (req, res, next) => { - // render a page or something? - res.send('ok'); +// this route is expecting there to be a token in the hash +// see /views/password-reset-email.ejs +router.get('/password-reset', (req, res, next) => { + // TODO: store the redirect uri in the token or something fancy + // admins and regular users should probably be redirected to different places. + res.render('password-reset', {redirectUri: process.env.TALK_ROOT_URL}); }); router.get('*', (req, res) => { diff --git a/routes/api/actions/index.js b/routes/api/actions/index.js new file mode 100644 index 000000000..f2f9be390 --- /dev/null +++ b/routes/api/actions/index.js @@ -0,0 +1,19 @@ +const express = require('express'); +const Action = require('../../../models/action'); + +const router = express.Router(); + +router.delete('/:action_id', (req, res, next) => { + Action + .findOneAndRemove({ + id: req.params.action_id + }) + .then(() => { + res.status(204).end(); + }) + .catch(error => { + next(error); + }); +}); + +module.exports = router; diff --git a/routes/api/comments/index.js b/routes/api/comments/index.js index 99957e987..d318857d6 100644 --- a/routes/api/comments/index.js +++ b/routes/api/comments/index.js @@ -1,147 +1,111 @@ const express = require('express'); const Comment = require('../../../models/comment'); +const wordlist = require('../../../services/wordlist'); const router = express.Router(); -//============================================================================== -// Get Routes -//============================================================================== - router.get('/', (req, res, next) => { - Comment.find({}).then((comments) => { - res.status(200).json(comments); + let query; + + if (req.query.status) { + query = Comment.findByStatus(req.query.status); + } else if (req.query.action_type) { + query = Comment.findByActionType(req.query.action_type); + } else { + query = Comment.all(); + } + + query.then(comments => { + res.json(comments); }) - .catch(next); + .catch((err) => { + next(err); + }); +}); + +router.post('/', wordlist.filter('body'), (req, res, next) => { + + const { + body, + asset_id, + parent_id, + author_id + } = req.body; + + Comment + .create({ + body, + asset_id, + parent_id, + status: req.wordlist.matched ? 'rejected' : '', + author_id + }) + .then((comment) => { + + res.status(201).send(comment); + }) + .catch((err) => { + next(err); + }); }); router.get('/:comment_id', (req, res, next) => { Comment .findById(req.params.comment_id) .then(comment => { + if (!comment) { + res.status(404).end(); + return; + } + res.status(200).json(comment); }) - .catch(next); -}); - -// Get all the comments that have an action_type over them. -router.get('/action/:action_type', (req, res, next) => { - Comment - .findByActionType(req.params.action_type) - .then((comments) => { - res.status(200).json(comments); - }) - .catch(next); -}); - -// Get all the comments that were rejected. -router.get('/status/rejected', (req, res, next) => { - Comment.findByStatus('rejected').then(comments => { - res.status(200).json(comments); - }) - .catch(next); -}); - -// Get all the comments that were accepted. -router.get('/status/accepted', (req, res, next) => { - Comment.findByStatus('accepted').then((comments) => { - res.status(200).json(comments); - }) - .catch(error => { - next(error); - }); -}); - -// Get all the not moderated comments. -router.get('/status/new', (req, res, next) => { - Comment.findByStatus('').then((comments) => { - res.status(200).json(comments); - }) - .catch(error => { - next(error); - }); -}); - -//============================================================================== -// Post Routes -//============================================================================== - -router.post('/', (req, res, next) => { - - const {body, author_id, asset_id, parent_id, status, username} = req.body; - - Comment - .new(body, author_id, asset_id, parent_id, status, username) - .then((comment) => { - res.status(200).send({'id': comment.id}); - }) - .catch(error => { - next(error); + .catch((err) => { + next(err); }); }); -router.post('/:comment_id', (req, res, next) => { - Comment - .findById(req.params.comment_id) - .then((comment) => { - comment.body = req.body.body; - comment.author_id = req.body.author_id; - comment.asset_id = req.body.asset_id; - comment.parent_id = req.body.parent_id; - comment.status = req.body.status; - return comment.save(); - }) - .then((comment) => { - res.status(200).send(comment); - }) - .catch(error => { - next(error); - }); -}); - -router.post('/:comment_id/status', (req, res, next) => { - - Comment - .changeStatus(req.params.comment_id, req.body.status) - .then(comment => res.status(200).send(comment)) - .catch(error => next(error)); - -}); - -router.post('/:comment_id/actions', (req, res, next) => { - Comment - .addAction(req.params.comment_id, req.body.user_id, req.body.action_type) - .then((action) => { - res.status(200).send(action); - }) - .catch(error => { - next(error); - }); -}); - -//============================================================================== -// Delete Routes -//============================================================================== - router.delete('/:comment_id', (req, res, next) => { Comment .removeById(req.params.comment_id) .then(() => { - res.status(201).send({}); + res.status(204).end(); }) - .catch(error => { - next(error); + .catch((err) => { + next(err); }); }); -router.delete('/:comment_id/actions', (req, res, next) => { - console.log(req.params); +router.put('/:comment_id/status', (req, res, next) => { + + const { + status + } = req.body; + Comment - .removeAction(req.params.comment_id, req.body.user_id, req.body.action_type) + .changeStatus(req.params.comment_id, status) .then(() => { - res.status(201).send({}); + res.status(204).end(); }) - .catch(error => { - next(error); + .catch((err) => { + next(err); + }); +}); + +router.post('/:comment_id/actions', (req, res, next) => { + + const { + user_id, + action_type + } = req.body; + + Comment + .addAction(req.params.comment_id, user_id, action_type) + .then((action) => { + res.status(201).json(action); + }) + .catch((err) => { + next(err); }); }); diff --git a/routes/api/index.js b/routes/api/index.js index c49e52c9d..7192a1324 100644 --- a/routes/api/index.js +++ b/routes/api/index.js @@ -9,5 +9,6 @@ router.use('/queue', require('./queue')); router.use('/settings', require('./settings')); router.use('/stream', require('./stream')); router.use('/user', require('./user')); +router.use('/actions', require('./actions')); module.exports = router; diff --git a/routes/api/stream/index.js b/routes/api/stream/index.js index b06e3566c..acbfe3d77 100644 --- a/routes/api/stream/index.js +++ b/routes/api/stream/index.js @@ -1,4 +1,5 @@ const express = require('express'); +const _ = require('lodash'); const Comment = require('../../../models/comment'); const User = require('../../../models/user'); @@ -25,9 +26,9 @@ router.get('/', (req, res, next) => { case 'pre': return Promise.all([Comment.findAcceptedByAssetId(asset.id), asset]); case 'post': - return Promise.all([Comment.findAcceptedAndNewByAssetId(asset.id), asset]); + return Promise.all([Comment.findAcceptedAndNewByAssetId(asset.id), asset]); default: - throw new Error('Moderation setting not found.'); + return Promise.reject(new Error('Moderation setting not found.')); } }) // Get all the users and actions for those comments. @@ -35,8 +36,12 @@ router.get('/', (req, res, next) => { return Promise.all([ [asset], comments, - User.findPublicByIdArray(comments.map((comment) => comment.author_id)), - Action.getActionSummaries(comments.map((comment) => comment.id)) + User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))), + Action.getActionSummaries(_.uniq([ + asset.id, + ...comments.map((comment) => comment.id), + ...comments.map((comment) => comment.author_id) + ])) ]); }) .then(([assets, comments, users, actions]) => { diff --git a/routes/api/user/index.js b/routes/api/user/index.js index 39bb8a5c9..3d5b49d93 100644 --- a/routes/api/user/index.js +++ b/routes/api/user/index.js @@ -79,6 +79,10 @@ router.post('/', (req, res, next) => { router.post('/update-password', (req, res, next) => { const {token, password} = req.body; + if (!password || password.length < 8) { + return res.status(400).send('Password must be at least 8 characters'); + } + User.verifyPasswordResetToken(token) .then(user => { return User.changePassword(user.id, password); @@ -100,7 +104,7 @@ router.post('/request-password-reset', (req, res, next) => { const {email} = req.body; if (!email) { - return next(); + return next('you must submit an email when requesting a password.'); } User diff --git a/services/wordlist.js b/services/wordlist.js new file mode 100644 index 000000000..e6f2ad668 --- /dev/null +++ b/services/wordlist.js @@ -0,0 +1,164 @@ +const debug = require('debug')('talk:services:wordlist'); +const _ = require('lodash'); +const natural = require('natural'); +const tokenizer = new natural.WordTokenizer(); +const Setting = require('../models/setting'); + +/** + * The root wordlist object. + * @type {Object} + */ +const wordlist = { + list: [], + enabled: false +}; + +/** + * Loads wordlists in from the naughty-words package based on languages + * selected. + * @param {Array} languages language codes to add to the wordlist + */ +wordlist.init = () => { + return Setting + .getSettings() + .then((settings) => { + + // Insert the settings wordlist. + wordlist.insert(settings.wordlist); + }); +}; + +/** + * Inserts the wordlist data and enables the wordlist. + * @param {Array} list list of words to be added to the wordlist + */ +wordlist.insert = (list) => { + + // Add the words to this array, but also lowercase the words so that an + // easy comparison can take place. + wordlist.list = _.uniq(wordlist.list.concat(list.map((word) => { + return tokenizer.tokenize(word.toLowerCase()); + }))); + + debug(`Added ${list.length} words to the wordlist, now the wordlist is ${wordlist.list.length} entries long.`); + + // Enable the wordlist. + wordlist.enabled = true; + + return Promise.resolve(wordlist); +}; + +/** + * Tests the phrase to see if it contains any of the defined blockwords. + * @param {String} phrase value to check for blockwords. + * @return {Boolean} true if a blockword is found, false otherwise. + */ +wordlist.match = (phrase) => { + + // Lowercase the word to ensure that we don't miss a match due to + // capitalization. + let lowerPhraseWords = tokenizer.tokenize(phrase.toLowerCase()); + + // This will return true in the event that at least one blockword is found + // in the phrase. + return wordlist.list.some((blockphrase) => { + + // First, let's see if we can find the first word in the blockphrase in the + // source phrase. + let idx = lowerPhraseWords.indexOf(blockphrase[0]); + + if (idx === -1) { + + // The first blockword in the blockphrase did not match the source phrase + // anywhere. + return false; + } + + // Here we'll quick respond with true in the event that the blockphrase was + // just a single word. + if (blockphrase.length === 1) { + return true; + } + + // We found the first word in the source phrase! Lets ensure it matches the + // rest of the blockphrase... + + // Check to see if it even has the length to support this word! + if (lowerPhraseWords.length < idx + blockphrase.length - 1) { + + // We couldn't possibly have the entire phrase here because we don't have + // enough entries! + return false; + } + + for (let i = 1; i < blockphrase.length; i++) { + + // Check to see if the next word also matches! + if (lowerPhraseWords[idx + i] !== blockphrase[i]) { + return false; + } + } + + // We've walked over all the words of the blockphrase, and haven't had a + // mismatch... It does contain the whole word! + return true; + }); +}; + +// ErrContainsProfanity is returned in the event that the middleware detects +// profanity/wordlisted words in the payload. +const ErrContainsProfanity = new Error('contains profanity'); +ErrContainsProfanity.status = 400; + +/** + * Connect middleware for scanning request bodies for wordlisted words and + * attaching a ErrContainsProfanity to the req.wordlisted parameter, otherwise + * it will just set that parameter to false. + * @param {Array} fields selectors for the body to extract the fields to be + * tested + * @return {Function} the Connect middleware + */ +wordlist.filter = (...fields) => (req, res, next) => { + + // Start with the sensible default that the content does not contain + // profanity. + req.wordlist = { + matched: false + }; + + // If the wordlist isn't enabled, then don't actually perform checking and + // forward the request! + if (!wordlist.enabled) { + return next(); + } + + // Loop over all the fields from the body that we want to check. + const containsProfanity = fields.some((field) => { + let phrase = _.get(req.body, field, false); + + // If the field doesn't exist in the body, then it can't be profane! + if (!phrase) { + + // Return that there wasn't a profane word here. + return false; + } + + // Check if the field contains a profane word. + if (wordlist.match(phrase)) { + debug(`the field "${field}" contained a phrase "${phrase}" which contained a wordlisted word/phrase`); + return true; + } + + return false; + }); + + // The body could contain some profanity, address that here. + if (containsProfanity) { + req.wordlist.matched = ErrContainsProfanity; + } + + next(); +}; + +module.exports = wordlist; +module.exports.ErrContainsProfanity = ErrContainsProfanity; diff --git a/swagger.yaml b/swagger.yaml index 9f25c6f42..fd3b0012b 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -7,221 +7,320 @@ host: talk-stg.coralproject.net schemes: - https basePath: /api/v1 +consumes: + - application/json produces: - application/json + paths: /comments: - # get: - # tags: - # - Comments - # produces: - # - application/json - # summary: Comment Types - # description: | - # This endpoint retrieves comments - # parameters: - # - name: id - # in: query - # description: Comment by id - # required: false - # type: string - # responses: - # 200: - # description: An array of comments - # schema: - # type: array - # items: - # $ref: '#/definitions/Comment' + get: + tags: + - Comments + parameters: + - name: status + in: query + description: Performs a search based on the comment's status. + type: string + enum: + - flag + - name: action_type + in: query + description: Performs a search based on the actions that have been added to it. + type: string + enum: + - rejected + - accepted + - new + responses: + 200: + description: Comments matching the query. + schema: + type: array + items: + - $ref: '#/definitions/Comment' + 500: + description: An error occured. + schema: + $ref: '#/definitions/Error' post: - description: Add a new comment + tags: + - Comments parameters: - name: body in: body - description: Body - required: true + description: The comment to create. schema: $ref: '#/definitions/Comment' responses: 201: - description: "OK: Comment Added" + description: The comment that was created. schema: - $ref: '#/definitions/Comment' + $ref: '#/definitions/Comment' 500: - description: "Error" + description: An error occured. + schema: + $ref: '#/definitions/Error' + /comments/{comment_id}: + get: + tags: + - Comments + parameters: + - name: comment_id + in: path + description: The id of the comment to retrieve. + type: string + required: true + responses: + 200: + description: The comment was found. + schema: + $ref: '#/definitions/Comment' + 404: + description: The comment was not found. + 500: + description: An error occured. + schema: + $ref: '#/definitions/Error' + delete: + tags: + - Comments + parameters: + - name: comment_id + in: path + description: The id of the comment to delete. + type: string + required: true + responses: + 204: + description: The comment was deleted. + 500: + description: An error occured. + schema: + $ref: '#/definitions/Error' + /comments/{comment_id}/status: + put: + tags: + - Comments + - Moderation + parameters: + - name: comment_id + in: path + description: The id of the comment to retrieve. + type: string + required: true + - name: body + in: body + description: The status to update to. + required: true + schema: + type: object + properties: + status: + type: string + description: The status to update to. + responses: + 204: + description: The comment status was updated. + 500: + description: An error occured. + schema: + $ref: '#/definitions/Error' /comments/{comment_id}/actions: post: tags: - Comments - description: Add a action + - Actions parameters: - name: comment_id in: path - description: Comment ID - required: true + description: The id of the comment to retrieve. type: string + required: true - name: body in: body - description: comment + description: The action to add. required: true schema: - $ref: '#/definitions/Action' + type: object + properties: + action_type: + type: string + description: The action to add responses: 201: - description: Action Added + description: The action created. schema: - type: array - items: - $ref: '#/definitions/Comment' - /comments/{comment_id}/status: - post: + $ref: '#/definitions/Action' + 500: + description: An error occured. + schema: + $ref: '#/definitions/Error' + /actions/{action_id}: + delete: tags: - - Comments - description: Add a new status + - Actions parameters: - - name: comment_id + - name: action_id in: path - description: Comment ID - required: true + description: The id of the action to delete. type: string - - name: body - in: body - description: comment required: true - schema: - $ref: '#/definitions/ModerationAction' responses: 204: - description: ModerationAction Added - /queue: + description: The action was deleted. + 500: + description: An error occured. + schema: + $ref: '#/definitions/Error' + /auth: get: tags: - - Queue - description: Queue - parameters: - - name: type - in: query - description: - "pending: no status | flagged: flagged action + no status | rejected: rejected status" - required: true - type: string - enum: - - pending - - flagged - - rejected - - name: limit - in: query - description: Queue limit - required: false - type: integer - - name: skip - in: query - description: Skip - required: false - type: integer + - Auth + description: Retrieves the current authentication credentials. responses: 200: - description: ModerationAction Added + description: The current user. + schema: + $ref: '#/definitions/User' + 500: + description: An error occured. + schema: + $ref: '#/definitions/Error' + delete: + tags: + - Auth + description: Logs out the current authenticated user. + responses: + 204: + description: The current user has been logged out. + 500: + description: An error occured. + schema: + $ref: '#/definitions/Error' + /auth/local: + post: + tags: + - Auth + parameters: + - name: body + in: body + required: true + description: The login credentials. + schema: + type: object + properties: + email: + type: string + description: The email address of the current user. + password: + type: string + description: The password of the current user. + responses: + 200: + description: The user has authenticated sucesfully. + schema: + $ref: '#/definitions/User' + 401: + description: The authentication error. + schema: + $ref: '#/definitions/Error' + 500: + description: An error occured. + schema: + $ref: '#/definitions/Error' + /auth/facebook: + get: + tags: + - Auth + responses: + 302: + description: Redirects the user to perform external facebook authentication. + 500: + description: An error occured. + schema: + $ref: '#/definitions/Error' + /queue/comments/pending: + get: + tags: + - Comments + - Moderation + responses: + 200: + description: The comments that are not moderated. schema: type: array items: - $ref: '#/definitions/ModerationAction' + - $ref: '#/definitions/Comment' + 500: + description: An error occured. + schema: + $ref: '#/definitions/Error' /stream: get: tags: - - Stream - description: Stream + - Actions + - Assets + - Comments + - Users parameters: - - name: asset_id + - name: asset_url in: query - description: Description - required: true + description: The asset url to get the comment stream from. type: string + format: url responses: 200: - description: OK + description: The comment stream. schema: - type: array - items: - $ref: '#/definitions/Item' + type: object + properties: + assets: + type: array + items: + - $ref: '#/definitions/Asset' + comments: + type: array + items: + - $ref: '#/definitions/Comment' + users: + type: array + items: + - $ref: '#/definitions/User' + actions: + type: array + items: + - $ref: '#/definitions/Actions' + 500: + description: An error occured. + schema: + $ref: '#/definitions/Error' /settings: get: - tags: - - Settings - description: Settings responses: 200: - description: Get Setting + description: The settings. schema: - type: array - items: - $ref: '#/definitions/Setting' + $ref: '#/definitions/Settings' + 500: + description: An error occured. + schema: + $ref: '#/definitions/Error' put: - tags: - - Settings - description: Settings responses: 204: - description: OK - - /user/request-password-reset: - post: - tags: - - Users - description: trigger a reset password email. sends a success code whether email was found or no. - responses: - 204: - description: OK - - /user/update-password: - post: - tags: - - Users - description: Update existing user password - parameters: - - name: token - type: string - in: body - description: JSON Web token taken taken from emailed link - required: true - - name: password - type: string - in: body - description: new password to be settings - required: true - responses: - 204: - description: OK - - /asset: - get: - tags: - - Asset - description: Get an asset by id. - responses: - 200: - description: OK - put: - tags: - - Asset - description: Upsert an asset. - responses: - 204: - description: OK - /asset?url={url}: - get: - tags: - - Asset - parameters: - - name: url - in: query - description: The url of the asset. - required: true - description: Get an asset by its url. - responses: - 200: - description: OK - + description: The settings were updated. + 500: + description: An error occured. + schema: + $ref: '#/definitions/Error' definitions: + Error: + type: object + properties: + message: + type: string + description: The error that occured. Item: type: object ModerationAction: @@ -314,5 +413,10 @@ definitions: type: string description: An array of the authors for this asset. publication_date: - type: date - desctipion: When this asset was published. + type: string + format: datetime + description: When this asset was published. + User: + type: object + Settings: + type: object diff --git a/tests/client/coral-framework/store/itemActions.spec.js b/tests/client/coral-framework/store/itemActions.spec.js index a6f14b65f..340ec9549 100644 --- a/tests/client/coral-framework/store/itemActions.spec.js +++ b/tests/client/coral-framework/store/itemActions.spec.js @@ -175,7 +175,7 @@ describe('itemActions', () => { fetchMock.delete('*', {}); return actions.deleteAction('abc', 'flag', '123', 'comments')(store.dispatch) .then(response => { - expect(fetchMock.calls().matched[0][0]).to.equal('/api/v1/comments/abc/actions'); + expect(fetchMock.calls().matched[0][0]).to.equal('/api/v1/actions/abc'); expect(response).to.deep.equal({}); }); }); diff --git a/tests/routes/api/comments/index.js b/tests/routes/api/comments/index.js index cde4b9efb..ec9e05d03 100644 --- a/tests/routes/api/comments/index.js +++ b/tests/routes/api/comments/index.js @@ -10,6 +10,7 @@ const expect = chai.expect; chai.should(); chai.use(require('chai-http')); +const wordlist = require('../../../../services/wordlist'); const Comment = require('../../../../models/comment'); const Action = require('../../../../models/action'); const User = require('../../../../models/user'); @@ -64,13 +65,13 @@ describe('Get /comments', () => { ]); }); - it('should return all the comments', function(done){ - chai.request(app) + it('should return all the comments', () => { + return chai.request(app) .get('/api/v1/comments') - .end(function(err, res){ - expect(err).to.be.null; + .then((res) => { + expect(res).to.have.status(200); - done(); + }); }); }); @@ -122,48 +123,42 @@ describe('Get comments by status and action', () => { ]); }); - it('should return all the rejected comments', function(done){ - chai.request(app) - .get('/api/v1/comments/status/rejected') - .end(function(err, res){ - expect(err).to.be.null; + it('should return all the rejected comments', () => { + return chai.request(app) + .get('/api/v1/comments?status=rejected') + .then((res) => { expect(res).to.have.status(200); expect(res.body[0]).to.have.property('id', 'abc'); - done(); }); }); - it('should return all the approved comments', function(done){ - chai.request(app) - .get('/api/v1/comments/status/accepted') - .end(function(err, res){ - expect(err).to.be.null; + it('should return all the approved comments', () => { + return chai.request(app) + .get('/api/v1/comments?status=accepted') + .then((res) => { expect(res).to.have.status(200); expect(res.body[0]).to.have.property('id', 'hij'); - done(); }); }); - it('should return all the new comments', function(done){ - chai.request(app) - .get('/api/v1/comments/status/new') - .end(function(err, res){ - expect(err).to.be.null; + it('should return all the new comments', () => { + return chai.request(app) + .get('/api/v1/comments?status=new') + .then((res) => { expect(res).to.have.status(200); expect(res.body[0]).to.have.property('id', 'def'); - done(); }); }); - it('should return all the flagged comments', function(done){ - chai.request(app) - .get('/api/v1/comments/action/flag') - .end(function(err, res){ + it('should return all the flagged comments', () => { + return chai.request(app) + .get('/api/v1/comments?action_type=flag') + .then((res) => { expect(res).to.have.status(200); - expect(err).to.be.null; + expect(res.body.length).to.equal(1); expect(res.body[0]).to.have.property('id', 'abc'); - done(); + }); }); }); @@ -190,18 +185,31 @@ describe('Post /comments', () => { beforeEach(() => { return Promise.all([ User.createLocalUsers(users), - Action.create(actions) + Action.create(actions), + wordlist.insert([ + 'bad words' + ]) ]); }); - it('it should create a comment', function(done) { - chai.request(app) + it('should create a comment', () => { + return chai.request(app) .post('/api/v1/comments') .send({'body': 'Something body.', 'author_id': '123', 'asset_id': '1', 'parent_id': ''}) - .end(function(err, res){ - expect(res).to.have.status(200); + .then((res) => { + expect(res).to.have.status(201); expect(res.body).to.have.property('id'); - done(); + }); + }); + + it('should create a comment with a rejected status if it contains a bad word', () => { + return chai.request(app) + .post('/api/v1/comments') + .send({'body': 'bad words are the baddest', 'author_id': '123', 'asset_id': '1', 'parent_id': ''}) + .then((res) => { + expect(res).to.have.status(201); + expect(res.body).to.have.property('id'); + expect(res.body).to.have.property('status', 'rejected'); }); }); }); @@ -251,72 +259,14 @@ describe('Get /:comment_id', () => { ]); }); - it('should return the right comment for the comment_id', function(done){ - chai.request(app) + it('should return the right comment for the comment_id', () => { + return chai.request(app) .get('/api/v1/comments/abc') - .end(function(err, res){ - expect(err).to.be.null; + .then((res) => { expect(res).to.have.status(200); expect(res).to.have.property('body'); expect(res.body).to.have.property('body', 'comment 10'); - done(); - }); - }); -}); -describe('Put /:comment_id', () => { - - const comments = [{ - id: 'abc', - body: 'comment 10', - asset_id: 'asset', - author_id: '123' - }, { - id: 'def', - body: 'comment 20', - asset_id: 'asset', - author_id: '456' - }, { - id: 'hij', - body: 'comment 30', - asset_id: '456' - }]; - - const users = [{ - displayName: 'Ana', - email: 'ana@gmail.com', - password: '123' - }, { - displayName: 'Maria', - email: 'maria@gmail.com', - password: '123' - }]; - - const actions = [{ - action_type: 'flag', - item_id: 'abc' - }, { - action_type: 'like', - item_id: 'hij' - }]; - - beforeEach(() => { - return Promise.all([ - Comment.create(comments), - User.createLocalUsers(users), - Action.create(actions) - ]); - }); - - it('it should update comment', function(done) { - chai.request(app) - .post('/api/v1/comments/abc') - .send({'body': 'Something body.', 'author_id': '123', 'asset_id': '1', 'parent_id': ''}) - .end(function(err, res){ - expect(err).to.be.null; - expect(res).to.have.status(200); - expect(res.body).to.have.property('body', 'Something body.'); - done(); }); }); }); @@ -369,7 +319,7 @@ describe('Remove /:comment_id', () => { return chai.request(app) .delete('/api/v1/comments/abc') .then((res) => { - expect(res).to.have.status(201); + expect(res).to.have.status(204); return Comment.findById('abc'); }) @@ -384,7 +334,7 @@ process.on('unhandledRejection', (reason) => { console.error(reason); }); -describe('Post /:comment_id/status', () => { +describe('Put /:comment_id/status', () => { const comments = [{ id: 'abc', @@ -433,12 +383,11 @@ describe('Post /:comment_id/status', () => { it('it should update status', function() { return chai.request(app) - .post('/api/v1/comments/abc/status') + .put('/api/v1/comments/abc/status') .send({status: 'accepted'}) .then((res) => { - expect(res).to.have.status(200); - expect(res).to.have.body; - expect(res.body).to.have.property('status', 'accepted'); + expect(res).to.have.status(204); + expect(res.body).to.be.empty; }); }); }); @@ -495,7 +444,7 @@ describe('Post /:comment_id/actions', () => { .post('/api/v1/comments/abc/actions') .send({'user_id': '456', 'action_type': 'flag'}) .then((res) => { - expect(res).to.have.status(200); + expect(res).to.have.status(201); expect(res).to.have.body; expect(res.body).to.have.property('item_type', 'comment'); expect(res.body).to.have.property('action_type', 'flag'); diff --git a/tests/services/wordlist.js b/tests/services/wordlist.js new file mode 100644 index 000000000..0ae76c176 --- /dev/null +++ b/tests/services/wordlist.js @@ -0,0 +1,119 @@ +const expect = require('chai').expect; + +const wordlist = require('../../services/wordlist'); + +describe('wordlist: services', () => { + + before(() => wordlist.insert([ + 'BAD', + 'bad', + 'how to murder', + 'how to kill' + ])); + + beforeEach(() => { + expect(wordlist.list).to.not.be.empty; + expect(wordlist.enabled).to.be.true; + }); + + describe('#init', () => { + + it('has entries', () => { + expect(wordlist.list).to.not.be.empty; + expect(wordlist.enabled).to.be.true; + }); + + }); + + describe('#match', () => { + + it('does match on a bad word', () => { + [ + 'how to kill', + 'what is bad', + 'bad', + 'BAD.', + 'how to murder', + 'How To mUrDer' + ].forEach((word) => { + expect(wordlist.match(word)).to.be.true; + }); + }); + + it('does not match on a good word', () => { + [ + 'how to', + 'kill', + 'bads', + 'how to be a great person?', + 'how to not kill?' + ].forEach((word) => { + expect(wordlist.match(word)).to.be.false; + }); + }); + + }); + + describe('#filter', () => { + + it('matches on bodies containing bad words', (done) => { + + let req = { + body: { + content: 'how to kill?' + } + }; + + wordlist.filter('content')(req, {}, (err) => { + expect(err).to.be.undefined; + expect(req).to.have.property('wordlist'); + expect(req.wordlist).to.have.property('matched'); + expect(req.wordlist.matched).to.be.equal(wordlist.ErrContainsProfanity); + + done(); + }); + + }); + + it('does not match on bodies not containing bad words', (done) => { + + let req = { + body: { + content: 'how to be a great person?' + } + }; + + wordlist.filter('content')(req, {}, (err) => { + expect(err).to.be.undefined; + expect(req).to.have.property('wordlist'); + expect(req.wordlist).to.have.property('matched'); + expect(req.wordlist.matched).to.be.false; + + done(); + }); + + }); + + it('does not match on bodies not containing the bad word field', (done) => { + + let req = { + body: { + author: 'how to kill?', + content: 'how to be a great person?' + } + }; + + wordlist.filter('content')(req, {}, (err) => { + expect(err).to.be.undefined; + expect(req).to.have.property('wordlist'); + expect(req.wordlist).to.have.property('matched'); + expect(req.wordlist.matched).to.be.false; + + done(); + }); + + }); + + }); + +}); diff --git a/views/password-reset-email.ejs b/views/password-reset-email.ejs index 0637b6bb2..17ed9e39b 100644 --- a/views/password-reset-email.ejs +++ b/views/password-reset-email.ejs @@ -1,6 +1,6 @@

We received a request to reset your password. If you did not request this change, you can ignore this email.
-If you did, please click here to reset password.

+If you did, please click here to reset password.

<% if (process.env.NODE_ENV !== 'production') { %>

<%= token %>

<% } %> diff --git a/views/password-reset.ejs b/views/password-reset.ejs new file mode 100644 index 000000000..de23bc16d --- /dev/null +++ b/views/password-reset.ejs @@ -0,0 +1,135 @@ + + + + + + Password Reset + + + + + + +
+
+ Set new password + + + + +
foo
+
+
+ + + +