mirror of
https://github.com/wassname/talk.git
synced 2026-07-09 08:16:14 +08:00
Merge branch 'graphql' of github.com:coralproject/talk into graphql
This commit is contained in:
+3
-3
@@ -8,7 +8,7 @@ const program = require('commander');
|
||||
const pkg = require('../package.json');
|
||||
const parseDuration = require('parse-duration');
|
||||
const Table = require('cli-table');
|
||||
const Asset = require('../models/asset');
|
||||
const AssetModel = require('../models/asset');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const scraper = require('../services/scraper');
|
||||
const util = require('../util');
|
||||
@@ -22,7 +22,7 @@ util.onshutdown([
|
||||
* Lists all the assets registered in the database.
|
||||
*/
|
||||
function listAssets() {
|
||||
Asset
|
||||
AssetModel
|
||||
.find({})
|
||||
.sort({'created_at': 1})
|
||||
.then((asset) => {
|
||||
@@ -56,7 +56,7 @@ function refreshAssets(ageString) {
|
||||
const ageMs = parseDuration(ageString);
|
||||
const age = new Date(now - ageMs);
|
||||
|
||||
Asset.find({
|
||||
AssetModel.find({
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@
|
||||
|
||||
const program = require('commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const Setting = require('../models/setting');
|
||||
const SettingsService = require('../services/settings');
|
||||
const util = require('../util');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
@@ -24,8 +24,8 @@ program
|
||||
.action(() => {
|
||||
const defaults = {id: '1', moderation: 'pre'};
|
||||
|
||||
Setting
|
||||
.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true})
|
||||
SettingsService
|
||||
.init(defaults)
|
||||
.then(() => {
|
||||
console.log('Created settings object.');
|
||||
util.shutdown();
|
||||
|
||||
+22
-19
@@ -7,7 +7,8 @@
|
||||
const program = require('commander');
|
||||
const pkg = require('../package.json');
|
||||
const prompt = require('prompt');
|
||||
const User = require('../models/user');
|
||||
const UsersService = require('../services/users');
|
||||
const UserModel = require('../models/user');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('../util');
|
||||
const Table = require('cli-table');
|
||||
@@ -76,13 +77,15 @@ function createUser(options) {
|
||||
});
|
||||
})
|
||||
.then((result) => {
|
||||
return User.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim())
|
||||
return UsersService.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim())
|
||||
.then((user) => {
|
||||
console.log(`Created user ${user.id}.`);
|
||||
|
||||
if (result.role && result.role.length > 0) {
|
||||
return User
|
||||
.addRoleToUser(user.id, result.role.trim())
|
||||
let role = result.role ? result.role.trim() : null;
|
||||
|
||||
if (role && role.length > 0) {
|
||||
return UsersService
|
||||
.addRoleToUser(user.id, role)
|
||||
.then(() => {
|
||||
console.log(`Added the admin ${result.role.trim()} to User ${user.id}.`);
|
||||
util.shutdown();
|
||||
@@ -102,7 +105,7 @@ function createUser(options) {
|
||||
* Deletes a user.
|
||||
*/
|
||||
function deleteUser(userID) {
|
||||
User
|
||||
UserModel
|
||||
.findOneAndRemove({
|
||||
id: userID
|
||||
})
|
||||
@@ -148,7 +151,7 @@ function passwd(userID) {
|
||||
return;
|
||||
}
|
||||
|
||||
User
|
||||
UsersService
|
||||
.changePassword(userID, result.password)
|
||||
.then(() => {
|
||||
console.log('Password changed.');
|
||||
@@ -168,7 +171,7 @@ function updateUser(userID, options) {
|
||||
const updates = [];
|
||||
|
||||
if (options.email && typeof options.email === 'string' && options.email.length > 0) {
|
||||
let q = User.update({
|
||||
let q = UserModel.update({
|
||||
'id': userID,
|
||||
'profiles.provider': 'local'
|
||||
}, {
|
||||
@@ -181,7 +184,7 @@ function updateUser(userID, options) {
|
||||
}
|
||||
|
||||
if (options.name && typeof options.name === 'string' && options.name.length > 0) {
|
||||
let q = User.update({
|
||||
let q = UserModel.update({
|
||||
'id': userID
|
||||
}, {
|
||||
$set: {
|
||||
@@ -208,7 +211,7 @@ function updateUser(userID, options) {
|
||||
* Lists all the users registered in the database.
|
||||
*/
|
||||
function listUsers() {
|
||||
User
|
||||
UsersService
|
||||
.all()
|
||||
.then((users) => {
|
||||
let table = new Table({
|
||||
@@ -248,7 +251,7 @@ function listUsers() {
|
||||
* @param {String} srcUserID id of the user to which is the source of the merge
|
||||
*/
|
||||
function mergeUsers(dstUserID, srcUserID) {
|
||||
User
|
||||
UsersService
|
||||
.mergeUsers(dstUserID, srcUserID)
|
||||
.then(() => {
|
||||
console.log(`User ${srcUserID} was merged into user ${dstUserID}.`);
|
||||
@@ -266,7 +269,7 @@ function mergeUsers(dstUserID, srcUserID) {
|
||||
* @param {String} role the role to add
|
||||
*/
|
||||
function addRole(userID, role) {
|
||||
User
|
||||
UsersService
|
||||
.addRoleToUser(userID, role)
|
||||
.then(() => {
|
||||
console.log(`Added the ${role} role to User ${userID}.`);
|
||||
@@ -284,7 +287,7 @@ function addRole(userID, role) {
|
||||
* @param {String} role the role to remove
|
||||
*/
|
||||
function removeRole(userID, role) {
|
||||
User
|
||||
UsersService
|
||||
.removeRoleFromUser(userID, role)
|
||||
.then(() => {
|
||||
console.log(`Removed the ${role} role from User ${userID}.`);
|
||||
@@ -301,8 +304,8 @@ function removeRole(userID, role) {
|
||||
* @param {String} userID id of the user to ban
|
||||
*/
|
||||
function ban(userID) {
|
||||
User
|
||||
.setStatus(userID, 'banned', '')
|
||||
UsersService
|
||||
.setStatus(userID, 'BANNED')
|
||||
.then(() => {
|
||||
console.log(`Banned the User ${userID}.`);
|
||||
util.shutdown();
|
||||
@@ -318,8 +321,8 @@ function ban(userID) {
|
||||
* @param {String} userUD id of the user to remove the role from
|
||||
*/
|
||||
function unban(userID) {
|
||||
User
|
||||
.setStatus(userID, 'active', '')
|
||||
UsersService
|
||||
.setStatus(userID, 'ACTIVE')
|
||||
.then(() => {
|
||||
console.log(`Unban the User ${userID}.`);
|
||||
util.shutdown();
|
||||
@@ -335,7 +338,7 @@ function unban(userID) {
|
||||
* @param {String} userID the ID of a user to disable
|
||||
*/
|
||||
function disableUser(userID) {
|
||||
User
|
||||
UsersService
|
||||
.disableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was disabled.`);
|
||||
@@ -352,7 +355,7 @@ function disableUser(userID) {
|
||||
* @param {String} userID the ID of a user to enable
|
||||
*/
|
||||
function enableUser(userID) {
|
||||
User
|
||||
UsersService
|
||||
.enableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was enabled.`);
|
||||
|
||||
@@ -11,7 +11,7 @@ export const checkLogin = () => dispatch => {
|
||||
dispatch(checkLoginRequest());
|
||||
coralApi('/auth')
|
||||
.then(result => {
|
||||
const isAdmin = !!result.user.roles.filter(i => i === 'admin').length;
|
||||
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
|
||||
dispatch(checkLoginSuccess(result.user, isAdmin));
|
||||
})
|
||||
.catch(error => dispatch(checkLoginFailure(error)));
|
||||
|
||||
@@ -132,7 +132,7 @@ class Comment extends React.Component {
|
||||
{
|
||||
this.state.replyBoxVisible
|
||||
? <ReplyBox
|
||||
replyPostedHandler={() => {
|
||||
commentPostedHandler={() => {
|
||||
console.log('replyPostedHandler');
|
||||
this.setState({replyBoxVisible: false});
|
||||
refetch();
|
||||
|
||||
@@ -120,7 +120,7 @@ class Embed extends Component {
|
||||
{
|
||||
user
|
||||
? <CommentBox
|
||||
refetch={refetch}
|
||||
commentPostedHandler={refetch}
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import Pym from 'pym.js';
|
||||
import Pym from '../../../node_modules/pym.js';
|
||||
|
||||
export default new Pym.Child({polling: 100});
|
||||
|
||||
@@ -27,7 +27,7 @@ export const fetchSignIn = (formData) => (dispatch) => {
|
||||
dispatch(signInRequest());
|
||||
coralApi('/auth/local', {method: 'POST', body: formData})
|
||||
.then(({user}) => {
|
||||
const isAdmin = !!user.roles.filter(i => i === 'admin').length;
|
||||
const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length;
|
||||
dispatch(signInSuccess(user, isAdmin));
|
||||
dispatch(hideSignInDialog());
|
||||
dispatch(addItem(user, 'users'));
|
||||
@@ -132,7 +132,7 @@ export const checkLogin = () => dispatch => {
|
||||
throw new Error('Not logged in');
|
||||
}
|
||||
|
||||
const isAdmin = !!result.user.roles.filter(i => i === 'admin').length;
|
||||
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
|
||||
dispatch(checkLoginSuccess(result.user, isAdmin));
|
||||
})
|
||||
.catch(error => dispatch(checkLoginFailure(error)));
|
||||
|
||||
@@ -11,7 +11,7 @@ class CommentBox extends Component {
|
||||
|
||||
// updateItem: PropTypes.func,
|
||||
// comments: PropTypes.array,
|
||||
replyPostedHandler: PropTypes.func,
|
||||
commentPostedHandler: PropTypes.func,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
assetId: PropTypes.string.isRequired,
|
||||
parentId: PropTypes.string,
|
||||
@@ -32,7 +32,7 @@ class CommentBox extends Component {
|
||||
// child_id,
|
||||
// updateItem,
|
||||
// appendItemArray,
|
||||
replyPostedHandler,
|
||||
commentPostedHandler,
|
||||
postItem,
|
||||
assetId,
|
||||
parentId,
|
||||
@@ -79,8 +79,8 @@ class CommentBox extends Component {
|
||||
addNotification('success', 'Your comment has been posted.');
|
||||
}
|
||||
|
||||
if (replyPostedHandler) {
|
||||
replyPostedHandler();
|
||||
if (commentPostedHandler) {
|
||||
commentPostedHandler();
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
|
||||
@@ -3,10 +3,10 @@ import CommentBox from '../coral-plugin-commentbox/CommentBox';
|
||||
|
||||
const name = 'coral-plugin-replies';
|
||||
|
||||
const ReplyBox = ({styles, postItem, assetId, authorId, addNotification, parentId, replyPostedHandler}) => (
|
||||
const ReplyBox = ({styles, postItem, assetId, authorId, addNotification, parentId, commentPostedHandler}) => (
|
||||
<div className={`${name}-textarea`} style={styles && styles.container}>
|
||||
<CommentBox
|
||||
replyPostedHandler={replyPostedHandler}
|
||||
commentPostedHandler={commentPostedHandler}
|
||||
parentId={parentId}
|
||||
addNotification={addNotification}
|
||||
authorId={authorId}
|
||||
@@ -17,7 +17,7 @@ const ReplyBox = ({styles, postItem, assetId, authorId, addNotification, parentI
|
||||
);
|
||||
|
||||
ReplyBox.propTypes = {
|
||||
replyPostedHandler: PropTypes.func,
|
||||
commentPostedHandler: PropTypes.func,
|
||||
parentId: PropTypes.string,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
authorId: PropTypes.string.isRequired,
|
||||
|
||||
@@ -2,7 +2,7 @@ const DataLoader = require('dataloader');
|
||||
|
||||
const util = require('./util');
|
||||
|
||||
const Action = require('../../models/action');
|
||||
const ActionsService = require('../../services/actions');
|
||||
|
||||
/**
|
||||
* Looks up actions based on the requested id's all bounded by the user.
|
||||
@@ -11,7 +11,8 @@ const Action = require('../../models/action');
|
||||
* @return {Promise} resolves to the promises of the requested actions
|
||||
*/
|
||||
const genActionSummariessByItemID = ({user = {}}, item_ids) => {
|
||||
return Action.getActionSummaries(item_ids, user.id)
|
||||
return ActionsService
|
||||
.getActionSummaries(item_ids, user.id)
|
||||
.then(util.arrayJoinBy(item_ids, 'item_id'));
|
||||
};
|
||||
|
||||
|
||||
@@ -5,14 +5,15 @@ const errors = require('../../errors');
|
||||
const scraper = require('../../services/scraper');
|
||||
const util = require('./util');
|
||||
|
||||
const Asset = require('../../models/asset');
|
||||
const AssetModel = require('../../models/asset');
|
||||
const AssetsService = require('../../services/assets');
|
||||
|
||||
/**
|
||||
* Retrieves assets by an array of ids.
|
||||
* @param {Object} context the context of the request
|
||||
* @param {Array} ids array of ids to lookup
|
||||
*/
|
||||
const genAssetsByID = (context, ids) => Asset.find({
|
||||
const genAssetsByID = (context, ids) => AssetModel.find({
|
||||
id: {
|
||||
$in: ids
|
||||
}
|
||||
@@ -32,7 +33,7 @@ const findOrCreateAssetByURL = (context, asset_url) => {
|
||||
return Promise.reject(errors.ErrInvalidAssetURL);
|
||||
}
|
||||
|
||||
return Asset.findOrCreateByUrl(asset_url)
|
||||
return AssetsService.findOrCreateByUrl(asset_url)
|
||||
.then((asset) => {
|
||||
|
||||
// If the asset wasn't scraped before, scrape it! Otherwise just return
|
||||
@@ -58,6 +59,6 @@ module.exports = (context) => ({
|
||||
getByURL: (url) => findOrCreateAssetByURL(context, url),
|
||||
|
||||
getByID: new DataLoader((ids) => genAssetsByID(context, ids)),
|
||||
getAll: new util.SingletonResolver(() => Asset.find({}))
|
||||
getAll: new util.SingletonResolver(() => AssetModel.find({}))
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,14 +2,15 @@ const DataLoader = require('dataloader');
|
||||
|
||||
const util = require('./util');
|
||||
|
||||
const Action = require('../../models/action');
|
||||
const Comment = require('../../models/comment');
|
||||
const ActionModel = require('../../models/action');
|
||||
const CommentModel = require('../../models/comment');
|
||||
const CommentsService = require('../../services/comments');
|
||||
|
||||
/**
|
||||
* Retrieves comments by an array of asset id's.
|
||||
* @param {Array} ids array of ids to lookup
|
||||
*/
|
||||
const genCommentsByAssetID = (context, ids) => Comment.find({
|
||||
const genCommentsByAssetID = (context, ids) => CommentModel.find({
|
||||
asset_id: {
|
||||
$in: ids
|
||||
},
|
||||
@@ -23,7 +24,7 @@ const genCommentsByAssetID = (context, ids) => Comment.find({
|
||||
* Retrieves comments by an array of parent ids.
|
||||
* @param {Array} ids array of ids to lookup
|
||||
*/
|
||||
const genCommentsByParentID = (context, ids) => Comment.find({
|
||||
const genCommentsByParentID = (context, ids) => CommentModel.find({
|
||||
parent_id: {
|
||||
$in: ids
|
||||
},
|
||||
@@ -39,7 +40,7 @@ const getCommentsByStatusAndAssetID = (context, {status = null, asset_id = null}
|
||||
status = status.toLowerCase();
|
||||
}
|
||||
|
||||
return Comment.moderationQueue(status, asset_id);
|
||||
return CommentsService.moderationQueue(status, asset_id);
|
||||
};
|
||||
|
||||
const getCommentsByActionTypeAndAssetID = (context, {action_type, asset_id = null}) => {
|
||||
@@ -49,13 +50,13 @@ const getCommentsByActionTypeAndAssetID = (context, {action_type, asset_id = nul
|
||||
action_type = action_type.toLowerCase();
|
||||
}
|
||||
|
||||
return Action.find({
|
||||
return ActionModel.find({
|
||||
action_type,
|
||||
|
||||
// TODO: remove when we move the enum over to the uppercase.
|
||||
item_type: 'comments'
|
||||
}).then((actions) => {
|
||||
let comments = Comment.find({
|
||||
let comments = CommentModel.find({
|
||||
id: {
|
||||
$in: actions.map((action) => action.item_id)
|
||||
}
|
||||
@@ -69,7 +70,7 @@ const getCommentsByActionTypeAndAssetID = (context, {action_type, asset_id = nul
|
||||
});
|
||||
};
|
||||
|
||||
const genCommentsByAuthorID = (context, authorIDs) => Comment.find({
|
||||
const genCommentsByAuthorID = (context, authorIDs) => CommentModel.find({
|
||||
author_id: {
|
||||
$in: authorIDs
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const Settings = require('../../models/setting');
|
||||
const SettingsService = require('../../services/settings');
|
||||
|
||||
const util = require('./util');
|
||||
|
||||
@@ -8,5 +8,5 @@ const util = require('./util');
|
||||
* @return {Object} object of loaders
|
||||
*/
|
||||
module.exports = () => ({
|
||||
Settings: new util.SingletonResolver(() => Settings.retrieve())
|
||||
Settings: new util.SingletonResolver(() => SettingsService.retrieve())
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const DataLoader = require('dataloader');
|
||||
|
||||
const User = require('../../models/user');
|
||||
const UsersService = require('../../services/users');
|
||||
|
||||
const genUserByIDs = (context, ids) => User.findByIdArray(ids);
|
||||
const genUserByIDs = (context, ids) => UsersService.findByIdArray(ids);
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const Action = require('../../models/action');
|
||||
const ActionModel = require('../../models/action');
|
||||
const ActionsService = require('../../services/actions');
|
||||
|
||||
/**
|
||||
* Creates an action on a item.
|
||||
@@ -9,7 +10,7 @@ const Action = require('../../models/action');
|
||||
* @return {Promise} resolves to the action created
|
||||
*/
|
||||
const createAction = ({user = {}}, {item_id, item_type, action_type, metadata = {}}) => {
|
||||
return Action.insertUserAction({
|
||||
return ActionsService.insertUserAction({
|
||||
item_id,
|
||||
item_type,
|
||||
user_id: user.id,
|
||||
@@ -25,7 +26,7 @@ const createAction = ({user = {}}, {item_id, item_type, action_type, metadata =
|
||||
* @return {Promise} resolves when the action is deleted
|
||||
*/
|
||||
const deleteAction = ({user}, {id}) => {
|
||||
return Action.remove({
|
||||
return ActionModel.remove({
|
||||
id,
|
||||
user_id: user.id
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const errors = require('../../errors');
|
||||
const Asset = require('../../models/asset');
|
||||
const Comment = require('../../models/comment');
|
||||
|
||||
const AssetsService = require('../../services/assets');
|
||||
const CommentsService = require('../../services/comments');
|
||||
|
||||
const Wordlist = require('../../services/wordlist');
|
||||
|
||||
@@ -14,7 +15,7 @@ const Wordlist = require('../../services/wordlist');
|
||||
* @return {Promise} resolves to the created comment
|
||||
*/
|
||||
const createComment = ({user}, {body, asset_id, parent_id = null}, status = null) => {
|
||||
return Comment.publicCreate({
|
||||
return CommentsService.publicCreate({
|
||||
body,
|
||||
asset_id,
|
||||
parent_id,
|
||||
@@ -58,8 +59,8 @@ const resolveNewCommentStatus = (context, {asset_id, body}, wordlist = {}) => {
|
||||
if (wordlist.banned) {
|
||||
status = Promise.resolve('rejected');
|
||||
} else {
|
||||
status = Asset
|
||||
.rectifySettings(Asset.findById(asset_id).then((asset) => {
|
||||
status = AssetsService
|
||||
.rectifySettings(AssetsService.findById(asset_id).then((asset) => {
|
||||
if (!asset) {
|
||||
return Promise.reject(errors.ErrNotFound);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const User = require('../../models/user');
|
||||
const UsersService = require('../../services/users');
|
||||
|
||||
/**
|
||||
* Updates a users settings.
|
||||
@@ -7,7 +7,7 @@ const User = require('../../models/user');
|
||||
* @return {Promise}
|
||||
*/
|
||||
const updateUserSettings = ({user}, {bio}) => {
|
||||
return User.updateSettings(user.id, {bio});
|
||||
return UsersService.updateSettings(user.id, {bio});
|
||||
};
|
||||
|
||||
module.exports = (context) => {
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
const Setting = require('./models/setting');
|
||||
const SettingsService = require('./services/settings');
|
||||
|
||||
module.exports = () => Promise.all([
|
||||
|
||||
// Upsert the settings object.
|
||||
Setting.init({id: '1', moderation: 'pre', wordlist: {banned: [], suspect: []}})
|
||||
SettingsService.init({
|
||||
id: '1',
|
||||
moderation: 'pre',
|
||||
wordlist: {
|
||||
banned: [],
|
||||
suspect: []
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -17,7 +17,11 @@ const ErrNotAuthorized = require('../errors').ErrNotAuthorized;
|
||||
* @return {Boolean} true if the user has all the roles required, false
|
||||
* otherwise
|
||||
*/
|
||||
authorization.has = (user, ...roles) => roles.every((role) => user.roles.indexOf(role) >= 0);
|
||||
authorization.has = (user, ...roles) => roles.every((role) => {
|
||||
|
||||
// TODO: remove toUpperCase once we've migrated over the roles.
|
||||
return user.roles.indexOf(role.toUpperCase()) >= 0;
|
||||
});
|
||||
|
||||
/**
|
||||
* needed is a connect middleware layer that ensures that all requests coming
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
// The maximum depth to recurse into nested objects checking for mongoose
|
||||
// objects.
|
||||
const maxRecursion = 3;
|
||||
|
||||
/**
|
||||
* Middleware to wrap the `res.json` function to ensure that we can filter the
|
||||
* payload response first based on user and role.
|
||||
*/
|
||||
module.exports = (req, res, next) => {
|
||||
|
||||
/**
|
||||
* Updates the original document based on filtering out for roles.
|
||||
* @param {Mixed} o original object to be modified
|
||||
* @param {Integer} l current level of depth in the first object
|
||||
* @return {Mixed} (possibly) modified object
|
||||
*/
|
||||
const wrap = (o, d = 0) => {
|
||||
if (d > maxRecursion) {
|
||||
return o;
|
||||
}
|
||||
|
||||
// If this is an array, we need to walk over all the object's elements.
|
||||
if (Array.isArray(o)) {
|
||||
|
||||
// Map each of the array elements.
|
||||
return o.map((ob) => wrap(ob, d + 1));
|
||||
|
||||
} else if (o && o.constructor && o.constructor.name === 'model') {
|
||||
|
||||
// The object here is definitly a mongoose model.
|
||||
|
||||
// Check to see if it has a `filterForUser` method.
|
||||
if (typeof o.filterForUser === 'function') {
|
||||
|
||||
// The object here actually has the `filterForUser` function, so filter
|
||||
// the object!
|
||||
o = o.filterForUser(req.user);
|
||||
}
|
||||
|
||||
} else if (typeof o === 'object') {
|
||||
|
||||
// Iterate over the props, find only properties owned by the object.
|
||||
for (let prop in o) {
|
||||
|
||||
// If and only if the object owns the property do we actually pull the
|
||||
// property out.
|
||||
if (typeof o.hasOwnProperty === 'function' && o.hasOwnProperty(prop)) {
|
||||
|
||||
// Wrap the property with one more layer down.
|
||||
o[prop] = wrap(o[prop], d + 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return o;
|
||||
};
|
||||
|
||||
// Save a reference to the original json function.
|
||||
const json = res.json;
|
||||
|
||||
// Override the original json function.
|
||||
res.json = (payload) => {
|
||||
|
||||
// Restore the old pointer.
|
||||
res.json = json;
|
||||
|
||||
// Send it down the pipe after we've filtered it.
|
||||
res.json(wrap(payload));
|
||||
};
|
||||
|
||||
// Now that we've overridden the `res.json`, let's hand it down.
|
||||
next();
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
These are some settings we're planning on implementing in the future.
|
||||
I'm keeping them in this file for reference
|
||||
|
||||
```javascript
|
||||
anonymous_users: {type: Boolean, default: false},
|
||||
block_mute_enabled: {type: Boolean, default: false},
|
||||
comment_count: {type: Boolean, default: false},
|
||||
comment_editing_enabled: {type: Boolean, default: false},
|
||||
comments_hidden: {type: Boolean, default: false},
|
||||
community_guidelines: {type: Boolean, default: false},
|
||||
detailed_flags: {type: Boolean, default: false},
|
||||
emojis_enabled: {type: Boolean, default: false},
|
||||
following: {type: Boolean, default: false},
|
||||
likes_enabled: {type: Boolean, default: false},
|
||||
mentions: {type: Boolean, default: false},
|
||||
nested_replies: {type: Boolean, default: false},
|
||||
notification_timeout: {type: Number, default: 4500},
|
||||
permalinks: {type: Boolean, default: false},
|
||||
post_button_text: {type: String, default: 'Post'},
|
||||
pseudonyms: {type: Boolean, default: false},
|
||||
public_profile: {type: Boolean, default: false},
|
||||
reactions_enabled: {type: Boolean, default: false},
|
||||
reply_button_text: {type: String, default: 'Reply'},
|
||||
rich_content: {type: Boolean, default: false},
|
||||
show_staff_picks: {type: Boolean, default: false},
|
||||
up_down_voting: {type: Boolean, default: false},
|
||||
user_badges: {type: Boolean, default: false},
|
||||
user_mods_enabled: {type: Boolean, default: false},
|
||||
user_stats_enabled: {type: Boolean, default: false}
|
||||
```
|
||||
+19
-175
@@ -1,16 +1,32 @@
|
||||
const mongoose = require('../services/mongoose');
|
||||
const uuid = require('uuid');
|
||||
const _ = require('lodash');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const ACTION_TYPES = [
|
||||
'LIKE',
|
||||
'FLAG'
|
||||
];
|
||||
|
||||
const ITEM_TYPES = [
|
||||
'ASSETS',
|
||||
'COMMENTS',
|
||||
'USERS'
|
||||
];
|
||||
|
||||
const ActionSchema = new Schema({
|
||||
id: {
|
||||
type: String,
|
||||
default: uuid.v4,
|
||||
unique: true
|
||||
},
|
||||
action_type: String,
|
||||
item_type: String,
|
||||
action_type: {
|
||||
type: String,
|
||||
enum: ACTION_TYPES
|
||||
},
|
||||
item_type: {
|
||||
type: String,
|
||||
enum: ITEM_TYPES
|
||||
},
|
||||
item_id: String,
|
||||
user_id: String,
|
||||
metadata: Schema.Types.Mixed
|
||||
@@ -21,178 +37,6 @@ const ActionSchema = new Schema({
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds an action by the id.
|
||||
* @param {String} id identifier of the action (uuid)
|
||||
*/
|
||||
ActionSchema.statics.findById = function(id) {
|
||||
return Action.findOne({id});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an action.
|
||||
* @param {String} item_id identifier of the comment (uuid)
|
||||
* @param {String} user_id user id of the action (uuid)
|
||||
* @param {String} action the new action to the comment
|
||||
* @return {Promise}
|
||||
*/
|
||||
ActionSchema.statics.insertUserAction = (action) => {
|
||||
|
||||
// Actions are made unique by using a query that can be reproducable, i.e.,
|
||||
// not containing user inputable values.
|
||||
let query = {
|
||||
action_type: action.action_type,
|
||||
item_type: action.item_type,
|
||||
item_id: action.item_id,
|
||||
user_id: action.user_id
|
||||
};
|
||||
|
||||
// Create/Update the action.
|
||||
return Action.findOneAndUpdate(query, action, {
|
||||
|
||||
// Ensure that if it's new, we return the new object created.
|
||||
new: true,
|
||||
|
||||
// Perform an upsert in the event that this doesn't exist.
|
||||
upsert: true,
|
||||
|
||||
// Set the default values if not provided based on the mongoose models.
|
||||
setDefaultsOnInsert: true
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds actions in an array of ids.
|
||||
* @param {String} ids array of user identifiers (uuid)
|
||||
*/
|
||||
ActionSchema.statics.findByItemIdArray = function(item_ids) {
|
||||
return Action.find({
|
||||
'item_id': {$in: item_ids}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the action summaries for the given asset, and comments around the
|
||||
* given user id.
|
||||
* @param {[type]} asset_id [description]
|
||||
* @param {[type]} comments [description]
|
||||
* @param {String} [current_user_id=''] [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
ActionSchema.statics.getActionSummariesFromComments = (asset_id = '', comments, current_user_id = '') => {
|
||||
|
||||
// Get the user id's from the author id's as a unique array that gets
|
||||
// sorted.
|
||||
let userIDs = _.uniq(comments.map((comment) => comment.author_id)).sort();
|
||||
|
||||
// Fetch the actions for pretty much everything at this point.
|
||||
return Action.getActionSummaries(_.uniq([
|
||||
|
||||
// Actions can be on assets...
|
||||
asset_id,
|
||||
|
||||
// Comments...
|
||||
...comments.map((comment) => comment.id),
|
||||
|
||||
// Or Authors...
|
||||
...userIDs
|
||||
].filter((e) => e)), current_user_id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns summaries of actions for an array of ids
|
||||
* @param {String} ids array of user identifiers (uuid)
|
||||
*/
|
||||
ActionSchema.statics.getActionSummaries = function(item_ids, current_user_id = '') {
|
||||
return Action.aggregate([
|
||||
{
|
||||
|
||||
// only grab items that match the specified item id's
|
||||
$match: {
|
||||
item_id: {
|
||||
$in: item_ids
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$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'
|
||||
},
|
||||
|
||||
current_user: {
|
||||
$max: {
|
||||
$cond: {
|
||||
if: {
|
||||
$eq: ['$user_id', current_user_id],
|
||||
},
|
||||
then: '$$CURRENT',
|
||||
else: null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$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: '$current_user'
|
||||
}
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
/*
|
||||
* Finds all comments for a specific action.
|
||||
* @param {String} action_type type of action
|
||||
* @param {String} item_type type of item the action is on
|
||||
*/
|
||||
ActionSchema.statics.findByType = function(action_type, item_type) {
|
||||
return Action.find({
|
||||
'action_type': action_type,
|
||||
'item_type': item_type
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds all comments ids for a specific action.
|
||||
* @param {String} action_type type of action
|
||||
* @param {String} item_type type of item the action is on
|
||||
*/
|
||||
ActionSchema.statics.findCommentsIdByActionType = function(action_type, item_type) {
|
||||
return Action.find({
|
||||
'action_type': action_type,
|
||||
'item_type': item_type
|
||||
}, 'item_id');
|
||||
};
|
||||
|
||||
const Action = mongoose.model('Action', ActionSchema);
|
||||
|
||||
module.exports = Action;
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
const mongoose = require('../services/mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const Setting = require('./setting');
|
||||
|
||||
const uuid = require('uuid');
|
||||
|
||||
const AssetSchema = new Schema({
|
||||
@@ -68,18 +65,6 @@ AssetSchema.index({
|
||||
background: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds an asset by its id.
|
||||
* @param {String} id identifier of the asset (uuid).
|
||||
*/
|
||||
AssetSchema.statics.findById = (id) => Asset.findOne({id});
|
||||
|
||||
/**
|
||||
* Finds a asset by its url.
|
||||
* @param {String} url identifier of the asset (uuid).
|
||||
*/
|
||||
AssetSchema.statics.findByUrl = (url) => Asset.findOne({url});
|
||||
|
||||
/**
|
||||
* Returns true if the asset is closed, false else.
|
||||
*/
|
||||
@@ -87,85 +72,6 @@ AssetSchema.virtual('isClosed').get(function() {
|
||||
return this.closedAt && this.closedAt.getTime() <= new Date().getTime();
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieves the settings given an asset query and rectifies it against the
|
||||
* global settings.
|
||||
* @param {Promise} assetQuery an asset query that returns a single asset.
|
||||
* @return {Promise}
|
||||
*/
|
||||
AssetSchema.statics.rectifySettings = (assetQuery) => Promise.all([
|
||||
Setting.retrieve(),
|
||||
assetQuery
|
||||
]).then(([settings, asset]) => {
|
||||
|
||||
// If the asset exists and has settings then return the merged object.
|
||||
if (asset && asset.settings) {
|
||||
settings.merge(asset.settings);
|
||||
}
|
||||
|
||||
return settings;
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds a asset by its url.
|
||||
*
|
||||
* NOTE: This function has scalability concerns regarding mongoose's decision
|
||||
* always write {updated_at: new Date()} on every call to findOneAndUpdate
|
||||
* even though the update document exactly matches the query document... In
|
||||
* the future this function should never update, only findOneAndCreate but this
|
||||
* is not possible with the mongoose driver.
|
||||
*
|
||||
* @param {String} url identifier of the asset (uuid).
|
||||
* @return {Promise}
|
||||
*/
|
||||
AssetSchema.statics.findOrCreateByUrl = (url) => Asset.findOneAndUpdate({url}, {url}, {
|
||||
|
||||
// Ensure that if it's new, we return the new object created.
|
||||
new: true,
|
||||
|
||||
// Perform an upsert in the event that this doesn't exist.
|
||||
upsert: true,
|
||||
|
||||
// Set the default values if not provided based on the mongoose models.
|
||||
setDefaultsOnInsert: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the settings for the asset.
|
||||
* @param {[type]} id [description]
|
||||
* @param {[type]} settings [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
AssetSchema.statics.overrideSettings = (id, settings) => Asset.findOneAndUpdate({id}, {
|
||||
$set: {
|
||||
settings
|
||||
}
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds assets matching keywords on the model. If `value` is an empty string,
|
||||
* then it will not even perform a text search query.
|
||||
* @param {String} value string to search by.
|
||||
* @return {Promise}
|
||||
*/
|
||||
AssetSchema.statics.search = (value) => value.length === 0 ? Asset.find({}) : Asset.find({
|
||||
$text: {
|
||||
$search: value
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds multiple assets with matching ids
|
||||
* @param {Array} ids an array of Strings of asset_id
|
||||
* @return {Promise} resolves to list of Assets
|
||||
*/
|
||||
AssetSchema.statics.findMultipleById = function (ids) {
|
||||
const query = ids.map(id => ({id}));
|
||||
return Asset.find(query);
|
||||
};
|
||||
|
||||
const Asset = mongoose.model('Asset', AssetSchema);
|
||||
|
||||
module.exports = Asset;
|
||||
|
||||
+7
-229
@@ -1,8 +1,12 @@
|
||||
const mongoose = require('../services/mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
const _ = require('lodash');
|
||||
const uuid = require('uuid');
|
||||
const Action = require('./action');
|
||||
|
||||
const STATUSES = [
|
||||
'ACCEPTED',
|
||||
'REJECTED',
|
||||
'PREMOD',
|
||||
];
|
||||
|
||||
/**
|
||||
* The Mongo schema for a Comment Status.
|
||||
@@ -11,11 +15,7 @@ const Action = require('./action');
|
||||
const StatusSchema = new Schema({
|
||||
type: {
|
||||
type: String,
|
||||
enum: [
|
||||
'accepted',
|
||||
'rejected',
|
||||
'premod',
|
||||
],
|
||||
enum: STATUSES,
|
||||
},
|
||||
|
||||
// The User ID of the user that assigned the status.
|
||||
@@ -56,228 +56,6 @@ const CommentSchema = new Schema({
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* toJSON overrides to remove fields from the json
|
||||
* output.
|
||||
*/
|
||||
CommentSchema.options.toJSON = {};
|
||||
CommentSchema.options.toJSON.virtuals = true;
|
||||
CommentSchema.options.toJSON.hide = '_id';
|
||||
CommentSchema.options.toJSON.transform = (doc, ret, options) => {
|
||||
if (options.hide) {
|
||||
options.hide.split(' ').forEach((prop) => {
|
||||
delete ret[prop];
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the object for the given user only allowing those with the allowed
|
||||
* roles/permissions to access particular parameters.
|
||||
*/
|
||||
CommentSchema.method('filterForUser', function(user = false) {
|
||||
if (!user || !user.roles.includes('admin')) {
|
||||
return _.pick(this.toJSON(), ['id', 'body', 'asset_id', 'author_id', 'parent_id', 'status']);
|
||||
}
|
||||
|
||||
return this.toJSON();
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a new Comment that came from a public source.
|
||||
* @param {Mixed} comment either a single comment or an array of comments.
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.publicCreate = (comment) => {
|
||||
|
||||
// Check to see if this is an array of comments, if so map it out.
|
||||
if (Array.isArray(comment)) {
|
||||
return Promise.all(comment.map(Comment.publicCreate));
|
||||
}
|
||||
|
||||
const {
|
||||
body,
|
||||
asset_id,
|
||||
parent_id,
|
||||
status = null,
|
||||
author_id
|
||||
} = comment;
|
||||
|
||||
comment = new Comment({
|
||||
body,
|
||||
asset_id,
|
||||
parent_id,
|
||||
status_history: status ? [{
|
||||
type: status,
|
||||
created_at: new Date()
|
||||
}] : [],
|
||||
status,
|
||||
author_id
|
||||
});
|
||||
|
||||
return comment.save();
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a comment by the id.
|
||||
* @param {String} id identifier of comment (uuid)
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.findById = (id) => Comment.findOne({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 = (asset_id) => Comment.find({
|
||||
asset_id
|
||||
});
|
||||
|
||||
/**
|
||||
* findByAssetIdWithStatuses finds all the comments where the asset id matches
|
||||
* what's provided and the status is one of the ones listed in the statuses
|
||||
* array.
|
||||
* @param {String} asset_id the asset id to search by
|
||||
* @param {Array} [statuses=[]] the array of statuses to search by
|
||||
* @return {Promise} resolves to an array of comments
|
||||
*/
|
||||
CommentSchema.statics.findByAssetIdWithStatuses = (asset_id, statuses = []) => Comment.find({
|
||||
asset_id,
|
||||
status: {
|
||||
$in: statuses
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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 = (action_type) => Action
|
||||
.findCommentsIdByActionType(action_type, 'comment')
|
||||
.then((actions) => Comment.find({
|
||||
id: {
|
||||
$in: actions.map((a) => a.item_id)
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
* Find comment id's where the action type matches the argument.
|
||||
* @param {String} action_type the type of action that was performed on the comment
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.findIdsByActionType = (action_type) => Action
|
||||
.findCommentsIdByActionType(action_type, 'comments')
|
||||
.then((actions) => actions.map(a => a.item_id));
|
||||
|
||||
/**
|
||||
* Find comments by current status
|
||||
* @param {String} status status of the comment to search for
|
||||
* @return {Promise} resovles to comment array
|
||||
*/
|
||||
CommentSchema.statics.findByStatus = (status = null) => {
|
||||
return Comment.find({status});
|
||||
};
|
||||
|
||||
/**
|
||||
* Find comments that need to be moderated (aka moderation queue).
|
||||
* @param {String} asset_id
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.moderationQueue = (status, asset_id = null) => {
|
||||
|
||||
// Fetch the comments with statuses.
|
||||
let comments = Comment.findByStatus(status);
|
||||
|
||||
if (asset_id) {
|
||||
comments = comments.where('asset_id', asset_id);
|
||||
}
|
||||
|
||||
return comments;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pushes a new status in for the user.
|
||||
* @param {String} id identifier of the comment (uuid)
|
||||
* @param {String} status the new status of the comment
|
||||
* @param {String} assigned_by the user id for the user who performed the
|
||||
* moderation action
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.pushStatus = (id, status, assigned_by = null) => Comment.update({id}, {
|
||||
$push: {
|
||||
status_history: {
|
||||
type: status,
|
||||
created_at: new Date(),
|
||||
assigned_by
|
||||
}
|
||||
},
|
||||
$set: {status}
|
||||
});
|
||||
|
||||
/**
|
||||
* Add an action to the comment.
|
||||
* @param {String} item_id identifier of the comment (uuid)
|
||||
* @param {String} user_id user id of the action (uuid)
|
||||
* @param {String} action the new action to the comment
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.addAction = (item_id, user_id, action_type, metadata) => Action.insertUserAction({
|
||||
item_id,
|
||||
item_type: 'comments',
|
||||
user_id,
|
||||
action_type,
|
||||
metadata
|
||||
});
|
||||
|
||||
/**
|
||||
* 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 = (id) => Comment.remove({id});
|
||||
|
||||
/**
|
||||
* Remove an action from the comment.
|
||||
* @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 = (item_id, user_id, action_type) => Action.remove({
|
||||
action_type,
|
||||
item_type: 'comment',
|
||||
item_id,
|
||||
user_id
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns all the comments in the collection.
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.all = () => Comment.find();
|
||||
|
||||
/**
|
||||
* Returns all the comments by user
|
||||
* probably to be paginated at some point in the future
|
||||
* @return {Promise} array resolves to an array of comments by that user
|
||||
*/
|
||||
CommentSchema.statics.findByUserId = function (author_id, admin = false) {
|
||||
|
||||
// do not return un-published comments for non-admins
|
||||
let query = {author_id};
|
||||
|
||||
if (!admin) {
|
||||
query.$nor = [{status: 'premod'}, {status: 'rejected'}];
|
||||
}
|
||||
|
||||
return Comment.find(query);
|
||||
};
|
||||
|
||||
// Comment model.
|
||||
const Comment = mongoose.model('Comment', CommentSchema);
|
||||
|
||||
|
||||
+1
-71
@@ -1,6 +1,5 @@
|
||||
const mongoose = require('../services/mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
const _ = require('lodash');
|
||||
|
||||
const WordlistSchema = new Schema({
|
||||
banned: [String],
|
||||
@@ -64,12 +63,6 @@ const SettingSchema = new Schema({
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* toJSON provides settings overrides to this object's serialization methods.
|
||||
*/
|
||||
SettingSchema.options.toJSON = {};
|
||||
SettingSchema.options.toJSON.virtuals = true;
|
||||
|
||||
/**
|
||||
* Merges two settings objects.
|
||||
*/
|
||||
@@ -88,72 +81,9 @@ SettingSchema.method('merge', function(src) {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Filters the object for the given user only allowing those with the allowed
|
||||
* roles/permissions to access particular parameters.
|
||||
*/
|
||||
SettingSchema.method('filterForUser', function(user = false) {
|
||||
if (!user || !user.roles.includes('admin')) {
|
||||
return _.pick(this.toJSON(), [
|
||||
'moderation',
|
||||
'infoBoxEnable',
|
||||
'infoBoxContent',
|
||||
'closeTimeout',
|
||||
'closedMessage',
|
||||
'charCountEnable',
|
||||
'charCount',
|
||||
'requireEmailConfirmation'
|
||||
]);
|
||||
}
|
||||
|
||||
return this.toJSON();
|
||||
});
|
||||
|
||||
/**
|
||||
* The Mongo Mongoose object.
|
||||
*/
|
||||
const Setting = mongoose.model('Setting', SettingSchema);
|
||||
|
||||
/**
|
||||
* The Setting Service object exposing the Setting model.
|
||||
* @type {Object}
|
||||
*/
|
||||
const SettingService = module.exports = {};
|
||||
|
||||
/**
|
||||
* The selector used to uniquely identify the settings document.
|
||||
*/
|
||||
const selector = {id: '1'};
|
||||
|
||||
/**
|
||||
* Gets the entire settings record and sends it back
|
||||
* @return {Promise} settings the whole settings record
|
||||
*/
|
||||
SettingService.retrieve = () => Setting.findOne(selector);
|
||||
|
||||
/**
|
||||
* This will update the settings object with whatever you pass in
|
||||
* @param {object} setting a hash of whatever settings you want to update
|
||||
* @return {Promise} settings Promise that resolves to the entire (updated) settings object.
|
||||
*/
|
||||
SettingService.update = (settings) => Setting.findOneAndUpdate(selector, {
|
||||
$set: settings
|
||||
}, {
|
||||
upsert: true,
|
||||
new: true,
|
||||
setDefaultsOnInsert: true
|
||||
});
|
||||
|
||||
/**
|
||||
* This is run once when the app starts to ensure settings are populated.
|
||||
* @return {Promise} null initialize the global settings object
|
||||
*/
|
||||
SettingService.init = (defaults) => {
|
||||
|
||||
// Inject the defaults on top of the passed in defaults to ensure that the new
|
||||
// settings conform to the required selector.
|
||||
defaults = Object.assign({}, defaults, selector);
|
||||
|
||||
// Actually update the settings collection.
|
||||
return SettingService.update(defaults);
|
||||
};
|
||||
module.exports = Setting;
|
||||
|
||||
+22
-644
@@ -1,40 +1,18 @@
|
||||
const mongoose = require('../services/mongoose');
|
||||
const uuid = require('uuid');
|
||||
const _ = require('lodash');
|
||||
const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Action = require('./action');
|
||||
const Comment = require('./comment');
|
||||
const Wordlist = require('../services/wordlist');
|
||||
const errors = require('../errors');
|
||||
|
||||
const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm';
|
||||
const PASSWORD_RESET_JWT_SUBJECT = 'password_reset';
|
||||
|
||||
// SALT_ROUNDS is the number of rounds that the bcrypt algorithm will run
|
||||
// through during the salting process.
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
// USER_ROLES is the array of roles that is permissible as a user role.
|
||||
const USER_ROLES = [
|
||||
'admin',
|
||||
'moderator'
|
||||
'ADMIN',
|
||||
'MODERATOR'
|
||||
];
|
||||
|
||||
// USER_STATUSES is the list of statuses that are permitted for the user status.
|
||||
// USER_STATUS is the list of statuses that are permitted for the user status.
|
||||
const USER_STATUS = [
|
||||
'active',
|
||||
'banned'
|
||||
'ACTIVE',
|
||||
'BANNED'
|
||||
];
|
||||
|
||||
// In the event that the TALK_SESSION_SECRET is missing but we are testing, then
|
||||
// set the process.env.TALK_SESSION_SECRET.
|
||||
if (process.env.NODE_ENV === 'test' && !process.env.TALK_SESSION_SECRET) {
|
||||
process.env.TALK_SESSION_SECRET = 'keyboard cat';
|
||||
} else if (!process.env.TALK_SESSION_SECRET) {
|
||||
throw new Error('TALK_SESSION_SECRET must be defined to encode JSON Web Tokens and other auth functionality');
|
||||
}
|
||||
|
||||
// ProfileSchema is the mongoose schema defined as the representation of a
|
||||
// User's profile stored in MongoDB.
|
||||
const ProfileSchema = new mongoose.Schema({
|
||||
@@ -103,11 +81,18 @@ const UserSchema = new mongoose.Schema({
|
||||
|
||||
// Roles provides an array of roles (as strings) that is associated with a
|
||||
// user.
|
||||
roles: [String],
|
||||
roles: [{
|
||||
type: String,
|
||||
enum: USER_ROLES
|
||||
}],
|
||||
|
||||
// Status provides a string that says in which state the account is.
|
||||
// When the account is banned, the user login is disabled.
|
||||
status: {type: String, enum: USER_STATUS, default: 'active'},
|
||||
status: {
|
||||
type: String,
|
||||
enum: USER_STATUS,
|
||||
default: 'ACTIVE'
|
||||
},
|
||||
|
||||
// User's settings
|
||||
settings: {
|
||||
@@ -134,627 +119,20 @@ UserSchema.index({
|
||||
background: false
|
||||
});
|
||||
|
||||
/**
|
||||
* toJSON overrides to remove the password field from the json
|
||||
* output.
|
||||
*/
|
||||
UserSchema.options.toJSON = {};
|
||||
UserSchema.options.toJSON.hide = '_id password';
|
||||
UserSchema.options.toJSON.virtuals = true;
|
||||
UserSchema.options.toJSON.transform = (doc, ret, options) => {
|
||||
if (options.hide) {
|
||||
options.hide.split(' ').forEach((prop) => {
|
||||
delete ret[prop];
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the object for the given user only allowing those with the allowed
|
||||
* roles/permissions to access particular parameters.
|
||||
*/
|
||||
UserSchema.method('filterForUser', function(user = false) {
|
||||
if (!user || !user.roles.includes('admin')) {
|
||||
let allowed = ['id', 'displayName', 'settings', 'created_at', 'updated_at'];
|
||||
|
||||
if (user && user.id === this.id) {
|
||||
allowed.push('roles');
|
||||
}
|
||||
|
||||
return _.pick(this.toJSON(), allowed);
|
||||
}
|
||||
|
||||
return this.toJSON();
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns true if the user has all the roles specified.
|
||||
*/
|
||||
UserSchema.method('hasRoles', function(...roles) {
|
||||
return roles.every((role) => this.roles.indexOf(role) >= 0);
|
||||
return roles.every((role) => {
|
||||
|
||||
// TODO: remove toUpperCase() once we've migrated usage.
|
||||
return this.roles.indexOf(role.toUpperCase()) >= 0;
|
||||
});
|
||||
});
|
||||
|
||||
// Create the User model.
|
||||
const UserModel = mongoose.model('User', UserSchema);
|
||||
|
||||
// UserService is the interface for the application to interact with the
|
||||
// UserModel through.
|
||||
const UserService = module.exports = {};
|
||||
|
||||
/**
|
||||
* Finds a user given their email address that we have for them in the system
|
||||
* and ensures that the retuned user matches the password passed in as well.
|
||||
* @param {string} email - email to look up the user by
|
||||
* @param {string} password - password to match against the found user
|
||||
* @param {Function} done [description]
|
||||
*/
|
||||
UserService.findLocalUser = (email, password) => {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return Promise.reject('email is required for findLocalUser');
|
||||
}
|
||||
|
||||
return UserModel.findOne({
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id: email.toLowerCase(),
|
||||
provider: 'local'
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((user) => {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
bcrypt.compare(password, user.password, (err, res) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
return resolve(user);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges two users together by taking all the profiles on a given user and
|
||||
* pushing them into the source user followed by deleting the destination user's
|
||||
* user account. This will not merge the roles associated with the source user.
|
||||
* @param {String} dstUserID id of the user to which is the target of the merge
|
||||
* @param {String} srcUserID id of the user to which is the source of the merge
|
||||
* @return {Promise} resolves when the users are merged
|
||||
*/
|
||||
UserService.mergeUsers = (dstUserID, srcUserID) => {
|
||||
let srcUser, dstUser;
|
||||
|
||||
return Promise
|
||||
.all([
|
||||
UserModel.findOne({id: dstUserID}).exec(),
|
||||
UserModel.findOne({id: srcUserID}).exec()
|
||||
])
|
||||
.then((users) => {
|
||||
dstUser = users[0];
|
||||
srcUser = users[1];
|
||||
|
||||
srcUser.profiles.forEach((profile) => {
|
||||
dstUser.profiles.push(profile);
|
||||
});
|
||||
|
||||
return srcUser.remove();
|
||||
})
|
||||
.then(() => dstUser.save());
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a user given a social profile and if the user does not exist, creates
|
||||
* them.
|
||||
* @param {Object} profile - User social/external profile
|
||||
* @param {Function} done [description]
|
||||
*/
|
||||
UserService.findOrCreateExternalUser = (profile) => {
|
||||
return UserModel
|
||||
.findOne({
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id: profile.id,
|
||||
provider: profile.provider
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((user) => {
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
|
||||
// The user was not found, lets create them!
|
||||
user = new UserModel({
|
||||
displayName: profile.displayName,
|
||||
roles: [],
|
||||
profiles: [
|
||||
{
|
||||
id: profile.id,
|
||||
provider: profile.provider
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return user.save();
|
||||
});
|
||||
};
|
||||
|
||||
UserService.changePassword = (id, password) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve(hashedPassword);
|
||||
});
|
||||
})
|
||||
.then((hashedPassword) => {
|
||||
return UserModel.update({id}, {
|
||||
$inc: {__v: 1},
|
||||
$set: {
|
||||
password: hashedPassword
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates local users.
|
||||
* @param {Array} users Users to create
|
||||
* @return {Promise} Resolves with the users that were created
|
||||
*/
|
||||
UserService.createLocalUsers = (users) => {
|
||||
return Promise.all(users.map((user) => {
|
||||
return UserService
|
||||
.createLocalUser(user.email, user.password, user.displayName);
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Check the requested displayname for naughty words (currently in English) and special chars
|
||||
* @param {String} displayName word to be checked for profanity
|
||||
* @return {Promise} rejected if the machine's sensibilites are offended
|
||||
*/
|
||||
const isValidDisplayName = (displayName) => {
|
||||
const onlyLettersNumbersUnderscore = /^[a-z0-9_]+$/;
|
||||
|
||||
if (!displayName) {
|
||||
return Promise.reject(errors.ErrMissingDisplay);
|
||||
}
|
||||
|
||||
if (!onlyLettersNumbersUnderscore.test(displayName)) {
|
||||
|
||||
return Promise.reject(errors.ErrSpecialChars);
|
||||
}
|
||||
|
||||
// check for profanity
|
||||
return Wordlist.displayNameCheck(displayName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the local user with a given email, password, and name.
|
||||
* @param {String} email email of the new user
|
||||
* @param {String} password plaintext password of the new user
|
||||
* @param {String} displayName name of the display user
|
||||
* @param {Function} done callback
|
||||
*/
|
||||
UserService.createLocalUser = (email, password, displayName) => {
|
||||
|
||||
if (!email) {
|
||||
return Promise.reject(errors.ErrMissingEmail);
|
||||
}
|
||||
|
||||
email = email.toLowerCase().trim();
|
||||
displayName = displayName.toLowerCase().trim();
|
||||
|
||||
if (!password) {
|
||||
return Promise.reject(errors.ErrMissingPassword);
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return Promise.reject(errors.ErrPasswordTooShort);
|
||||
}
|
||||
|
||||
return isValidDisplayName(displayName)
|
||||
.then(() => { // displayName is valid
|
||||
return new Promise((resolve, reject) => {
|
||||
bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
let user = new UserModel({
|
||||
displayName: displayName,
|
||||
password: hashedPassword,
|
||||
roles: [],
|
||||
profiles: [
|
||||
{
|
||||
id: email,
|
||||
provider: 'local'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
user.save((err) => {
|
||||
if (err) {
|
||||
if (err.code === 11000) {
|
||||
if (err.message.match('displayName')) {
|
||||
return reject(errors.ErrDisplayTaken);
|
||||
}
|
||||
return reject(errors.ErrEmailTaken);
|
||||
}
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(user);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Disables a given user account.
|
||||
* @param {String} id id of a user
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
UserService.disableUser = (id) => {
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$set: {
|
||||
disabled: true
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Enables a given user account.
|
||||
* @param {String} id id of a user
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
UserService.enableUser = (id) => {
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$set: {
|
||||
disabled: false
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a role to a user.
|
||||
* @param {String} id id of a user
|
||||
* @param {String} role role to add
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
UserService.addRoleToUser = (id, role) => {
|
||||
|
||||
// Check to see if the user role is in the allowable set of roles.
|
||||
if (USER_ROLES.indexOf(role) === -1) {
|
||||
|
||||
// User role is not supported! Error out here.
|
||||
return Promise.reject(new Error(`role ${role} is not supported`));
|
||||
}
|
||||
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$addToSet: {
|
||||
roles: role
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a role from a user.
|
||||
* @param {String} id id of a user
|
||||
* @param {String} role role to remove
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
UserService.removeRoleFromUser = (id, role) => {
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$pull: {
|
||||
roles: role
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Set status of a user.
|
||||
* @param {String} id id of a user
|
||||
* @param {String} status status to set
|
||||
* @param {String} comment_id id of the comment that the user was ban for.
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
UserService.setStatus = (id, status, comment_id) => {
|
||||
|
||||
// Check to see if the user status is in the allowable set of roles.
|
||||
if (USER_STATUS.indexOf(status) === -1) {
|
||||
|
||||
// User status is not supported! Error out here.
|
||||
return Promise.reject(new Error(`status ${status} is not supported`));
|
||||
}
|
||||
|
||||
// If ban then reject the comment and update status
|
||||
if (status === 'banned') {
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$set: {
|
||||
status: status
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
return Comment.pushStatus(comment_id, 'rejected', id);
|
||||
});
|
||||
}
|
||||
|
||||
if (status === 'active') {
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$set: {
|
||||
status: status
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a user with the id.
|
||||
* @param {String} id user id (uuid)
|
||||
*/
|
||||
UserService.findById = (id) => {
|
||||
return UserModel.findOne({id});
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds users in an array of ids.
|
||||
* @param {Array} ids array of user identifiers (uuid)
|
||||
*/
|
||||
UserService.findByIdArray = (ids) => {
|
||||
return UserModel.find({
|
||||
'id': {$in: ids}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds public user information by an array of ids.
|
||||
* @param {Array} ids array of user identifiers (uuid)
|
||||
*/
|
||||
UserService.findPublicByIdArray = (ids) => {
|
||||
return UserModel.find({
|
||||
'id': {$in: ids}
|
||||
}, 'id displayName');
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a JWT from a user email. Only works for local accounts.
|
||||
* @param {String} email of the local user
|
||||
*/
|
||||
UserService.createPasswordResetToken = function (email) {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return Promise.reject('email is required when creating a JWT for resetting passord');
|
||||
}
|
||||
|
||||
email = email.toLowerCase();
|
||||
|
||||
return UserModel.findOne({profiles: {$elemMatch: {id: email}}})
|
||||
.then((user) => {
|
||||
if (!user) {
|
||||
|
||||
// Since we don't want to reveal that the email does/doesn't exist
|
||||
// just go ahead and resolve the Promise with null and check in the
|
||||
// endpoint.
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
jti: uuid.v4(),
|
||||
email,
|
||||
userId: user.id,
|
||||
version: user.__v
|
||||
};
|
||||
|
||||
return jwt.sign(payload, process.env.TALK_SESSION_SECRET, {
|
||||
algorithm: 'HS256',
|
||||
expiresIn: '1d',
|
||||
subject: PASSWORD_RESET_JWT_SUBJECT
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies that the token was indeed signed by the session secret.
|
||||
* @param {String} token JWT token from the client
|
||||
* @return {Promise}
|
||||
*/
|
||||
UserService.verifyToken = (token, options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// Set the allowed algorithms.
|
||||
options.algorithms = ['HS256'];
|
||||
|
||||
jwt.verify(token, process.env.TALK_SESSION_SECRET, options, (err, decoded) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve(decoded);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies a jwt and returns the associated user.
|
||||
* @param {String} token the JSON Web Token to verify
|
||||
*/
|
||||
UserService.verifyPasswordResetToken = (token) => {
|
||||
return UserService
|
||||
.verifyToken(token, {
|
||||
subject: PASSWORD_RESET_JWT_SUBJECT
|
||||
})
|
||||
.then((decoded) => UserService.findById(decoded.userId));
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a user using a value which gets compared using a prefix match against
|
||||
* the user's email address and/or their display name.
|
||||
* @param {String} value value to search by
|
||||
* @return {Promise}
|
||||
*/
|
||||
UserService.search = (value) => {
|
||||
return UserModel.find({
|
||||
$or: [
|
||||
|
||||
// Search by a prefix match on the displayName.
|
||||
{
|
||||
'displayName': {
|
||||
$regex: new RegExp(`^${value}`),
|
||||
$options: 'i'
|
||||
}
|
||||
},
|
||||
|
||||
// Search by a prefix match on the email address.
|
||||
{
|
||||
'profiles': {
|
||||
$elemMatch: {
|
||||
id: {
|
||||
$regex: new RegExp(`^${value}`),
|
||||
$options: 'i'
|
||||
},
|
||||
provider: 'local'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a count of the current users.
|
||||
* @return {Promise}
|
||||
*/
|
||||
UserService.count = () => UserModel.count();
|
||||
|
||||
/**
|
||||
* Returns all the users.
|
||||
* @return {Promise}
|
||||
*/
|
||||
UserService.all = () => UserModel.find();
|
||||
|
||||
/**
|
||||
* Updates the user's settings.
|
||||
* @return {Promise}
|
||||
*/
|
||||
UserService.updateSettings = (id, settings) => UserModel.update({
|
||||
id
|
||||
}, {
|
||||
$set: {
|
||||
settings
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Add an action to the user.
|
||||
* @param {String} item_id identifier of the user (uuid)
|
||||
* @param {String} user_id user id of the action (uuid)
|
||||
* @param {String} action the new action to the user
|
||||
* @return {Promise}
|
||||
*/
|
||||
UserService.addAction = (item_id, user_id, action_type, metadata) => Action.insertUserAction({
|
||||
item_id,
|
||||
item_type: 'users',
|
||||
user_id,
|
||||
action_type,
|
||||
metadata
|
||||
});
|
||||
|
||||
/**
|
||||
* This creates a token based around confirming the local profile.
|
||||
* @param {String} userID The user id for the user that we are creating the
|
||||
* token for.
|
||||
* @param {String} email The email that we are needing to get confirmed.
|
||||
* @return {Promise}
|
||||
*/
|
||||
UserService.createEmailConfirmToken = (userID, email) => {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return Promise.reject('email is required when creating a JWT for resetting passord');
|
||||
}
|
||||
|
||||
email = email.toLowerCase();
|
||||
|
||||
return UserService
|
||||
.findById(userID)
|
||||
.then((user) => {
|
||||
if (!user) {
|
||||
return Promise.reject(new Error('user not found'));
|
||||
}
|
||||
|
||||
// Get the profile representing the local account.
|
||||
let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local');
|
||||
|
||||
// Ensure that the user email hasn't already been verified.
|
||||
if (profile && profile.metadata && profile.metadata.confirmed_at) {
|
||||
return Promise.reject(new Error('email address already confirmed'));
|
||||
}
|
||||
|
||||
const payload = {
|
||||
email,
|
||||
userID
|
||||
};
|
||||
|
||||
return jwt.sign(payload, process.env.TALK_SESSION_SECRET, {
|
||||
jwtid: uuid.v4(),
|
||||
algorithm: 'HS256',
|
||||
expiresIn: '1d',
|
||||
subject: EMAIL_CONFIRM_JWT_SUBJECT
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This verifies that a given token was for the email confirmation and updates
|
||||
* that user's profile with a 'confirmed_at' parameter with the current date.
|
||||
* @param {String} token the token containing the email confirmation details
|
||||
* signed with our secret.
|
||||
* @return {Promise}
|
||||
*/
|
||||
UserService.verifyEmailConfirmation = (token) => {
|
||||
return UserService
|
||||
.verifyToken(token, {
|
||||
subject: EMAIL_CONFIRM_JWT_SUBJECT
|
||||
})
|
||||
.then(({userID, email}) => {
|
||||
|
||||
return UserModel
|
||||
.update({
|
||||
id: userID,
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id: email,
|
||||
provider: 'local'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
$set: {
|
||||
'profiles.$.metadata.confirmed_at': new Date()
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
module.exports = UserModel;
|
||||
module.exports.USER_ROLES = USER_ROLES;
|
||||
module.exports.USER_STATUS = USER_STATUS;
|
||||
|
||||
@@ -11,7 +11,7 @@ router.get('/password-reset', (req, res) => {
|
||||
});
|
||||
|
||||
router.get('/login', (req, res, next) => {
|
||||
res.render('login');
|
||||
res.render('admin/login');
|
||||
});
|
||||
|
||||
router.get('*', (req, res) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const User = require('../../../models/user');
|
||||
const UsersService = require('../../../services/users');
|
||||
const mailer = require('../../../services/mailer');
|
||||
const authorization = require('../../../middleware/authorization');
|
||||
const errors = require('../../../errors');
|
||||
@@ -26,7 +26,7 @@ router.post('/email/confirm', (req, res, next) => {
|
||||
return next(errors.ErrMissingToken);
|
||||
}
|
||||
|
||||
User
|
||||
UsersService
|
||||
.verifyEmailConfirmation(token)
|
||||
.then(() => {
|
||||
res.status(204).end();
|
||||
@@ -47,7 +47,7 @@ router.post('/password/reset', (req, res, next) => {
|
||||
return next('you must submit an email when requesting a password.');
|
||||
}
|
||||
|
||||
User
|
||||
UsersService
|
||||
.createPasswordResetToken(email)
|
||||
.then((token) => {
|
||||
|
||||
@@ -100,9 +100,10 @@ router.put('/password/reset', (req, res, next) => {
|
||||
return next(errors.ErrPasswordTooShort);
|
||||
}
|
||||
|
||||
User.verifyPasswordResetToken(token)
|
||||
UsersService
|
||||
.verifyPasswordResetToken(token)
|
||||
.then(user => {
|
||||
return User.changePassword(user.id, password);
|
||||
return UsersService.changePassword(user.id, password);
|
||||
})
|
||||
.then(() => {
|
||||
res.status(204).end();
|
||||
@@ -120,7 +121,7 @@ router.put('/settings', authorization.needed(), (req, res, next) => {
|
||||
bio
|
||||
} = req.body;
|
||||
|
||||
User
|
||||
UsersService
|
||||
.updateSettings(req.user.id, {bio})
|
||||
.then(() => {
|
||||
res.status(204).end();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const express = require('express');
|
||||
const Action = require('../../../models/action');
|
||||
const ActionsService = require('../../../services/actions');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.delete('/:action_id', (req, res, next) => {
|
||||
Action
|
||||
ActionsService
|
||||
.findOneAndRemove({
|
||||
id: req.params.action_id,
|
||||
user_id: req.user.id
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const Asset = require('../../../models/asset');
|
||||
const scraper = require('../../../services/scraper');
|
||||
const AssetsService = require('../../../services/assets');
|
||||
|
||||
const AssetModel = require('../../../models/asset');
|
||||
|
||||
// List assets.
|
||||
router.get('/', (req, res, next) => {
|
||||
@@ -46,13 +48,13 @@ router.get('/', (req, res, next) => {
|
||||
Promise.all([
|
||||
|
||||
// Find the actuall assets.
|
||||
FilterOpenAssets(Asset.search(search), filter)
|
||||
FilterOpenAssets(AssetsService.search(search), filter)
|
||||
.sort({[field]: (sort === 'asc') ? 1 : -1})
|
||||
.skip(parseInt(skip))
|
||||
.limit(parseInt(limit)),
|
||||
|
||||
// Get the count of actual assets.
|
||||
FilterOpenAssets(Asset.search(search), filter)
|
||||
FilterOpenAssets(AssetsService.search(search), filter)
|
||||
.count()
|
||||
])
|
||||
.then(([result, count]) => {
|
||||
@@ -73,7 +75,7 @@ router.get('/', (req, res, next) => {
|
||||
router.get('/:asset_id', (req, res, next) => {
|
||||
|
||||
// Send back the asset.
|
||||
Asset
|
||||
AssetsService
|
||||
.findById(req.params.asset_id)
|
||||
.then((asset) => {
|
||||
if (!asset) {
|
||||
@@ -91,7 +93,7 @@ router.get('/:asset_id', (req, res, next) => {
|
||||
router.post('/:asset_id/scrape', (req, res, next) => {
|
||||
|
||||
// Create a new asset scrape job.
|
||||
Asset
|
||||
AssetsService
|
||||
.findById(req.params.asset_id)
|
||||
.then((asset) => {
|
||||
if (!asset) {
|
||||
@@ -113,7 +115,7 @@ router.post('/:asset_id/scrape', (req, res, next) => {
|
||||
router.put('/:asset_id/settings', (req, res, next) => {
|
||||
|
||||
// Override the settings for the asset.
|
||||
Asset
|
||||
AssetsService
|
||||
.overrideSettings(req.params.asset_id, req.body)
|
||||
.then(() => res.status(204).end())
|
||||
.catch((err) => next(err));
|
||||
@@ -128,7 +130,7 @@ router.put('/:asset_id/status', (req, res, next) => {
|
||||
closedMessage
|
||||
} = req.body;
|
||||
|
||||
Asset
|
||||
AssetModel
|
||||
.update({id}, {
|
||||
$set: {
|
||||
closedAt,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
const express = require('express');
|
||||
const errors = require('../../../errors');
|
||||
const Comment = require('../../../models/comment');
|
||||
const Asset = require('../../../models/asset');
|
||||
const User = require('../../../models/user');
|
||||
const Action = require('../../../models/action');
|
||||
const wordlist = require('../../../services/wordlist');
|
||||
|
||||
const CommentModel = require('../../../models/comment');
|
||||
const CommentsService = require('../../../services/comments');
|
||||
const AssetsService = require('../../../services/assets');
|
||||
const UsersService = require('../../../services/users');
|
||||
const ActionsService = require('../../../services/actions');
|
||||
|
||||
const authorization = require('../../../middleware/authorization');
|
||||
const _ = require('lodash');
|
||||
|
||||
@@ -48,27 +50,27 @@ router.get('/', (req, res, next) => {
|
||||
// otherwise this will be a vulnerability if you pass user_id and something else,
|
||||
// the app will return admin-level data without the proper checks
|
||||
if (user_id) {
|
||||
query = Comment.findByUserId(user_id, authorization.has(req.user, 'admin'));
|
||||
query = CommentsService.findByUserId(user_id, authorization.has(req.user, 'admin'));
|
||||
} else if (status) {
|
||||
query = assetIDWrap(Comment.findByStatus(status === 'new' ? null : status));
|
||||
query = assetIDWrap(CommentsService.findByStatus(status === 'NEW' ? null : status));
|
||||
} else if (action_type) {
|
||||
query = Comment
|
||||
query = CommentsService
|
||||
.findIdsByActionType(action_type)
|
||||
.then((ids) => assetIDWrap(Comment.find({
|
||||
.then((ids) => assetIDWrap(CommentModel.find({
|
||||
id: {
|
||||
$in: ids
|
||||
}
|
||||
})));
|
||||
} else {
|
||||
query = assetIDWrap(Comment.all());
|
||||
query = assetIDWrap(CommentsService.all());
|
||||
}
|
||||
|
||||
query.then((comments) => {
|
||||
return Promise.all([
|
||||
comments,
|
||||
Asset.findMultipleById(comments.map(comment => comment.asset_id)),
|
||||
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
|
||||
Action.getActionSummariesFromComments(asset_id, comments, req.user ? req.user.id : false)
|
||||
AssetsService.findMultipleById(comments.map(comment => comment.asset_id)),
|
||||
UsersService.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
|
||||
ActionsService.getActionSummariesFromComments(asset_id, comments, req.user ? req.user.id : false)
|
||||
]);
|
||||
})
|
||||
.then(([comments, assets, users, actions]) =>
|
||||
@@ -83,79 +85,8 @@ router.get('/', (req, res, next) => {
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/', wordlist.filter('body'), (req, res, next) => {
|
||||
|
||||
const {
|
||||
body,
|
||||
asset_id,
|
||||
parent_id
|
||||
} = req.body;
|
||||
|
||||
// Decide the status based on whether or not the current asset/settings
|
||||
// has pre-mod enabled or not. If the comment was rejected based on the
|
||||
// wordlist, then reject it, otherwise if the moderation setting is
|
||||
// premod, set it to `premod`.
|
||||
let status;
|
||||
|
||||
if (req.wordlist.banned) {
|
||||
status = Promise.resolve('rejected');
|
||||
} else {
|
||||
status = Asset
|
||||
.rectifySettings(Asset.findById(asset_id).then((asset) => {
|
||||
if (!asset) {
|
||||
return Promise.reject(errors.ErrNotFound);
|
||||
}
|
||||
|
||||
// Check to see if the asset has closed commenting...
|
||||
if (asset.isClosed) {
|
||||
|
||||
// They have, ensure that we send back an error.
|
||||
return Promise.reject(new errors.ErrAssetCommentingClosed(asset.closedMessage));
|
||||
}
|
||||
|
||||
return asset;
|
||||
}))
|
||||
|
||||
// Return `premod` if pre-moderation is enabled and an empty "new" status
|
||||
// in the event that it is not in pre-moderation mode.
|
||||
.then(({moderation, charCountEnable, charCount}) => {
|
||||
|
||||
// Reject if the comment is too long
|
||||
if (charCountEnable && body.length > charCount) {
|
||||
return 'rejected';
|
||||
}
|
||||
return moderation === 'pre' ? 'premod' : null;
|
||||
});
|
||||
}
|
||||
|
||||
status.then((status) => Comment.publicCreate({
|
||||
body,
|
||||
asset_id,
|
||||
parent_id,
|
||||
status,
|
||||
author_id: req.user.id
|
||||
}))
|
||||
.then((comment) => {
|
||||
if (req.wordlist.suspect) {
|
||||
return Comment
|
||||
.addAction(comment.id, null, 'flag', {field: 'body', details: 'Matched suspect word filters.'})
|
||||
.then(() => comment);
|
||||
}
|
||||
|
||||
return comment;
|
||||
})
|
||||
.then((comment) => {
|
||||
|
||||
// The comment was created! Send back the created comment.
|
||||
res.status(201).json(comment);
|
||||
})
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:comment_id', authorization.needed('admin'), (req, res, next) => {
|
||||
Comment
|
||||
CommentsService
|
||||
.findById(req.params.comment_id)
|
||||
.then(comment => {
|
||||
if (!comment) {
|
||||
@@ -171,7 +102,7 @@ router.get('/:comment_id', authorization.needed('admin'), (req, res, next) => {
|
||||
});
|
||||
|
||||
router.delete('/:comment_id', authorization.needed('admin'), (req, res, next) => {
|
||||
Comment
|
||||
CommentsService
|
||||
.removeById(req.params.comment_id)
|
||||
.then(() => {
|
||||
res.status(204).end();
|
||||
@@ -186,7 +117,7 @@ router.put('/:comment_id/status', authorization.needed('admin'), (req, res, next
|
||||
status
|
||||
} = req.body;
|
||||
|
||||
Comment
|
||||
CommentsService
|
||||
.pushStatus(req.params.comment_id, status, req.user.id)
|
||||
.then(() => {
|
||||
res.status(204).end();
|
||||
@@ -203,7 +134,7 @@ router.post('/:comment_id/actions', (req, res, next) => {
|
||||
metadata
|
||||
} = req.body;
|
||||
|
||||
Comment
|
||||
CommentsService
|
||||
.addAction(req.params.comment_id, req.user.id, action_type, metadata)
|
||||
.then((action) => {
|
||||
res.status(201).json(action);
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
const express = require('express');
|
||||
const authorization = require('../../middleware/authorization');
|
||||
const payloadFilter = require('../../middleware/payload-filter');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Filter all content going down the pipe based on user roles.
|
||||
router.use(payloadFilter);
|
||||
|
||||
router.use('/assets', authorization.needed('admin'), require('./assets'));
|
||||
router.use('/settings', authorization.needed('admin'), require('./settings'));
|
||||
router.use('/queue', authorization.needed('admin'), require('./queue'));
|
||||
@@ -15,7 +11,6 @@ router.use('/comments', authorization.needed(), require('./comments'));
|
||||
router.use('/actions', authorization.needed(), require('./actions'));
|
||||
|
||||
router.use('/auth', require('./auth'));
|
||||
router.use('/stream', require('./stream'));
|
||||
router.use('/users', require('./users'));
|
||||
router.use('/account', require('./account'));
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const express = require('express');
|
||||
const Comment = require('../../../models/comment');
|
||||
const User = require('../../../models/user');
|
||||
const Action = require('../../../models/action');
|
||||
const CommentsService = require('../../../services/comments');
|
||||
const CommentModel = require('../../../models/comment');
|
||||
const UsersService = require('../../../services/users');
|
||||
const ActionsService = require('../../../services/actions');
|
||||
const authorization = require('../../../middleware/authorization');
|
||||
const _ = require('lodash');
|
||||
|
||||
@@ -10,8 +11,8 @@ const router = express.Router();
|
||||
function gatherActionsAndUsers (comments) {
|
||||
return Promise.all([
|
||||
comments,
|
||||
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
|
||||
Action.getActionSummaries(_.uniq([
|
||||
UsersService.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
|
||||
ActionsService.getActionSummaries(_.uniq([
|
||||
...comments.map((comment) => comment.id),
|
||||
...comments.map((comment) => comment.author_id)
|
||||
]))
|
||||
@@ -30,7 +31,7 @@ router.get('/comments/pending', authorization.needed('admin'), (req, res, next)
|
||||
|
||||
const {asset_id} = req.query;
|
||||
|
||||
Comment.moderationQueue('premod', asset_id)
|
||||
CommentsService.moderationQueue('PREMOD', asset_id)
|
||||
.then(gatherActionsAndUsers)
|
||||
.then(([comments, users, actions]) => {
|
||||
res.json({comments, users, actions});
|
||||
@@ -43,7 +44,7 @@ router.get('/comments/pending', authorization.needed('admin'), (req, res, next)
|
||||
router.get('/comments/rejected', authorization.needed('admin'), (req, res, next) => {
|
||||
const {asset_id} = req.query;
|
||||
|
||||
Comment.moderationQueue('rejected', asset_id)
|
||||
CommentsService.moderationQueue('REJECTED', asset_id)
|
||||
.then(gatherActionsAndUsers)
|
||||
.then(([comments, users, actions]) => {
|
||||
res.json({comments, users, actions});
|
||||
@@ -64,8 +65,8 @@ router.get('/comments/flagged', authorization.needed('admin'), (req, res, next)
|
||||
return query;
|
||||
};
|
||||
|
||||
Comment.findIdsByActionType('flag')
|
||||
.then(ids => assetIDWrap(Comment.find({
|
||||
CommentsService.findIdsByActionType('FLAG')
|
||||
.then(ids => assetIDWrap(CommentModel.find({
|
||||
id: {$in: ids}
|
||||
})))
|
||||
.then(gatherActionsAndUsers)
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
const express = require('express');
|
||||
const Setting = require('../../../models/setting');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', (req, res, next) => {
|
||||
Setting.retrieve().then((settings) => {
|
||||
res.json(settings);
|
||||
})
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
SettingsService
|
||||
.retrieve().then((settings) => {
|
||||
res.json(settings);
|
||||
})
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
router.put('/', (req, res, next) => {
|
||||
Setting.update(req.body).then(() => {
|
||||
res.status(204).end();
|
||||
})
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
SettingsService
|
||||
.update(req.body).then(() => {
|
||||
res.status(204).end();
|
||||
})
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
const express = require('express');
|
||||
const _ = require('lodash');
|
||||
const scraper = require('../../../services/scraper');
|
||||
const errors = require('../../../errors');
|
||||
const url = require('url');
|
||||
|
||||
const Comment = require('../../../models/comment');
|
||||
const User = require('../../../models/user');
|
||||
const Action = require('../../../models/action');
|
||||
const Asset = require('../../../models/asset');
|
||||
const Setting = require('../../../models/setting');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', (req, res, next) => {
|
||||
|
||||
let asset_url = decodeURIComponent(req.query.asset_url);
|
||||
|
||||
// Verify that the asset_url is parsable.
|
||||
let parsed_asset_url = url.parse(asset_url);
|
||||
if (!parsed_asset_url.protocol) {
|
||||
return next(errors.ErrInvalidAssetURL);
|
||||
}
|
||||
|
||||
// Get the asset_id for this url (or create it if it doesn't exist)
|
||||
Promise.all([
|
||||
|
||||
// Find or create the asset by url.
|
||||
Asset.findOrCreateByUrl(asset_url)
|
||||
|
||||
// Add the found asset to the scraper if it's not already scraped.
|
||||
.then((asset) => {
|
||||
if (!asset.scraped) {
|
||||
return scraper.create(asset).then(() => asset);
|
||||
}
|
||||
|
||||
return asset;
|
||||
}),
|
||||
|
||||
// Get the moderation setting from the settings.
|
||||
Setting.retrieve()
|
||||
])
|
||||
.then(([asset, settings]) => {
|
||||
|
||||
// Merge the asset specific settings with the returned settings object in
|
||||
// the event that the asset that was returned also had settings.
|
||||
if (asset && asset.settings) {
|
||||
settings.merge(asset.settings);
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
|
||||
// This is the promised component... Fetch the comments based on the
|
||||
// moderation settings.
|
||||
Comment.findByAssetIdWithStatuses(asset.id, [null, 'accepted']),
|
||||
|
||||
// Send back the reference to the asset.
|
||||
asset,
|
||||
|
||||
// Send back the settings to the stream.
|
||||
settings
|
||||
]);
|
||||
})
|
||||
|
||||
// Get all the users and actions for those comments.
|
||||
.then(([comments, asset, settings]) => {
|
||||
|
||||
// Get the user id's from the author id's as a unique array that gets
|
||||
// sorted.
|
||||
let userIDs = _.uniq(comments.map((comment) => comment.author_id)).sort();
|
||||
|
||||
// Fetch the users for which there is a comment available for them.
|
||||
let users = userIDs.length > 0 ? User.findByIdArray(userIDs) : [];
|
||||
|
||||
// Fetch the actions for pretty much everything at this point.
|
||||
let actions = Action.getActionSummariesFromComments(asset.id, comments, req.user ? req.user.id : false);
|
||||
|
||||
return Promise.all([
|
||||
|
||||
// Pass back the asset that we loaded...
|
||||
asset,
|
||||
|
||||
// It's comments...
|
||||
comments,
|
||||
|
||||
// The users who wrote those comments
|
||||
users,
|
||||
|
||||
// And all actions about the asset, comments, and users.
|
||||
actions,
|
||||
|
||||
// Pass back the settings that we loaded.
|
||||
settings
|
||||
]);
|
||||
})
|
||||
.then(([asset, comments, users, actions, settings]) => {
|
||||
|
||||
// Send back the payload containing all this data.
|
||||
res.json({
|
||||
assets: [asset],
|
||||
comments,
|
||||
users,
|
||||
actions,
|
||||
settings
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+18
-13
@@ -1,7 +1,8 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const User = require('../../../models/user');
|
||||
const Setting = require('../../../models/setting');
|
||||
const UsersService = require('../../../services/users');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
const CommentsService = require('../../../services/comments');
|
||||
const mailer = require('../../../services/mailer');
|
||||
const authorization = require('../../../middleware/authorization');
|
||||
|
||||
@@ -15,12 +16,12 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
|
||||
} = req.query;
|
||||
|
||||
Promise.all([
|
||||
User
|
||||
UsersService
|
||||
.search(value)
|
||||
.sort({[field]: (asc === 'true') ? 1 : -1})
|
||||
.skip((page - 1) * limit)
|
||||
.limit(limit),
|
||||
User.count()
|
||||
UsersService.count()
|
||||
])
|
||||
.then(([result, count]) => {
|
||||
res.json({
|
||||
@@ -35,7 +36,7 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
|
||||
});
|
||||
|
||||
router.post('/:user_id/role', authorization.needed('admin'), (req, res, next) => {
|
||||
User
|
||||
UsersService
|
||||
.addRoleToUser(req.params.user_id, req.body.role)
|
||||
.then(() => {
|
||||
res.status(204).end();
|
||||
@@ -43,11 +44,15 @@ router.post('/:user_id/role', authorization.needed('admin'), (req, res, next) =>
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
router.post('/:user_id/status', (req, res, next) => {
|
||||
User
|
||||
.setStatus(req.params.user_id, req.body.status, req.body.comment_id)
|
||||
router.post('/:user_id/status', authorization.needed('admin'), (req, res, next) => {
|
||||
UsersService
|
||||
.setStatus(req.params.user_id, req.body.status)
|
||||
.then((status) => {
|
||||
res.status(201).json(status);
|
||||
|
||||
if (status === 'BANNED' && req.body.comment_id) {
|
||||
return CommentsService.pushStatus(req.body.comment_id, 'rejected', req.params.user_id);
|
||||
}
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
@@ -64,7 +69,7 @@ router.post('/:user_id/status', (req, res, next) => {
|
||||
* @param {String} userID the id for the user to send the email to
|
||||
* @param {String} email the email for the user to send the email to
|
||||
*/
|
||||
const SendEmailConfirmation = (app, userID, email) => User
|
||||
const SendEmailConfirmation = (app, userID, email) => UsersService
|
||||
.createEmailConfirmToken(userID, email)
|
||||
.then((token) => {
|
||||
return mailer.sendSimple({
|
||||
@@ -87,14 +92,14 @@ router.post('/', (req, res, next) => {
|
||||
displayName
|
||||
} = req.body;
|
||||
|
||||
User
|
||||
UsersService
|
||||
.createLocalUser(email, password, displayName)
|
||||
.then((user) => {
|
||||
|
||||
// Get the settings from the database to find out if we need to send an
|
||||
// email confirmation. The Front end will know about the
|
||||
// requireEmailConfirmation as it's included in the settings get endpoint.
|
||||
return Setting.retrieve().then(({requireEmailConfirmation = false}) => {
|
||||
return SettingsService.retrieve().then(({requireEmailConfirmation = false}) => {
|
||||
|
||||
if (requireEmailConfirmation) {
|
||||
|
||||
@@ -123,7 +128,7 @@ router.post('/:user_id/actions', authorization.needed(), (req, res, next) => {
|
||||
metadata
|
||||
} = req.body;
|
||||
|
||||
User
|
||||
UsersService
|
||||
.addAction(req.params.user_id, req.user.id, action_type, metadata)
|
||||
.then((action) => {
|
||||
res.status(201).json(action);
|
||||
@@ -138,7 +143,7 @@ router.post('/:user_id/email/confirm', authorization.needed('admin'), (req, res,
|
||||
user_id
|
||||
} = req.params;
|
||||
|
||||
User
|
||||
UsersService
|
||||
.findById(user_id)
|
||||
.then((user) => {
|
||||
if (!user) {
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
const ActionModel = require('../models/action');
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = class ActionsService {
|
||||
|
||||
/**
|
||||
* Finds an action by the id.
|
||||
* @param {String} id identifier of the action (uuid)
|
||||
*/
|
||||
static findById(id) {
|
||||
return ActionModel.findOne({id});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an action.
|
||||
* @param {String} item_id identifier of the comment (uuid)
|
||||
* @param {String} user_id user id of the action (uuid)
|
||||
* @param {String} action the new action to the comment
|
||||
* @return {Promise}
|
||||
*/
|
||||
static insertUserAction(action) {
|
||||
|
||||
// Actions are made unique by using a query that can be reproducable, i.e.,
|
||||
// not containing user inputable values.
|
||||
let query = {
|
||||
action_type: action.action_type,
|
||||
item_type: action.item_type,
|
||||
item_id: action.item_id,
|
||||
user_id: action.user_id
|
||||
};
|
||||
|
||||
// Create/Update the action.
|
||||
return ActionModel.findOneAndUpdate(query, action, {
|
||||
|
||||
// Ensure that if it's new, we return the new object created.
|
||||
new: true,
|
||||
|
||||
// Perform an upsert in the event that this doesn't exist.
|
||||
upsert: true,
|
||||
|
||||
// Set the default values if not provided based on the mongoose models.
|
||||
setDefaultsOnInsert: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds actions in an array of ids.
|
||||
* @param {String} ids array of user identifiers (uuid)
|
||||
*/
|
||||
static findByItemIdArray(item_ids) {
|
||||
return ActionModel.find({
|
||||
'item_id': {$in: item_ids}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the action summaries for the given asset, and comments around the
|
||||
* given user id.
|
||||
* @param {[type]} asset_id [description]
|
||||
* @param {[type]} comments [description]
|
||||
* @param {String} [current_user_id=''] [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
static getActionSummariesFromComments(asset_id = '', comments, current_user_id = '') {
|
||||
|
||||
// Get the user id's from the author id's as a unique array that gets
|
||||
// sorted.
|
||||
let userIDs = _.uniq(comments.map((comment) => comment.author_id)).sort();
|
||||
|
||||
// Fetch the actions for pretty much everything at this point.
|
||||
return ActionsService.getActionSummaries(_.uniq([
|
||||
|
||||
// Actions can be on assets...
|
||||
asset_id,
|
||||
|
||||
// Comments...
|
||||
...comments.map((comment) => comment.id),
|
||||
|
||||
// Or Authors...
|
||||
...userIDs
|
||||
].filter((e) => e)), current_user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns summaries of actions for an array of ids
|
||||
* @param {String} ids array of user identifiers (uuid)
|
||||
*/
|
||||
static getActionSummaries(item_ids, current_user_id = '') {
|
||||
return ActionModel.aggregate([
|
||||
{
|
||||
|
||||
// only grab items that match the specified item id's
|
||||
$match: {
|
||||
item_id: {
|
||||
$in: item_ids
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$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'
|
||||
},
|
||||
|
||||
current_user: {
|
||||
$max: {
|
||||
$cond: {
|
||||
if: {
|
||||
$eq: ['$user_id', current_user_id],
|
||||
},
|
||||
then: '$$CURRENT',
|
||||
else: null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$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: '$current_user'
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
/*
|
||||
* Finds all comments for a specific action.
|
||||
* @param {String} action_type type of action
|
||||
* @param {String} item_type type of item the action is on
|
||||
*/
|
||||
static findByType(action_type, item_type) {
|
||||
return ActionModel.find({
|
||||
'action_type': action_type,
|
||||
'item_type': item_type
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all comments ids for a specific action.
|
||||
* @param {String} action_type type of action
|
||||
* @param {String} item_type type of item the action is on
|
||||
*/
|
||||
static findCommentsIdByActionType(action_type, item_type) {
|
||||
return ActionModel.find({
|
||||
'action_type': action_type,
|
||||
'item_type': item_type
|
||||
}, 'item_id');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
const AssetModel = require('../models/asset');
|
||||
const SettingsService = require('./settings');
|
||||
|
||||
module.exports = class AssetsService {
|
||||
|
||||
/**
|
||||
* Finds an asset by its id.
|
||||
* @param {String} id identifier of the asset (uuid).
|
||||
*/
|
||||
static findById(id) {
|
||||
return AssetModel.findOne({id});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a asset by its url.
|
||||
* @param {String} url identifier of the asset (uuid).
|
||||
*/
|
||||
static findByUrl(url) {
|
||||
return AssetModel.findOne({url});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the settings given an asset query and rectifies it against the
|
||||
* global settings.
|
||||
* @param {Promise} assetQuery an asset query that returns a single asset.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static rectifySettings(assetQuery) {
|
||||
return Promise.all([
|
||||
SettingsService.retrieve(),
|
||||
assetQuery
|
||||
]).then(([settings, asset]) => {
|
||||
|
||||
// If the asset exists and has settings then return the merged object.
|
||||
if (asset && asset.settings) {
|
||||
settings.merge(asset.settings);
|
||||
}
|
||||
|
||||
return settings;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a asset by its url.
|
||||
*
|
||||
* NOTE: This function has scalability concerns regarding mongoose's decision
|
||||
* always write {updated_at: new Date()} on every call to findOneAndUpdate
|
||||
* even though the update document exactly matches the query document... In
|
||||
* the future this function should never update, only findOneAndCreate but this
|
||||
* is not possible with the mongoose driver.
|
||||
*
|
||||
* @param {String} url identifier of the asset (uuid).
|
||||
* @return {Promise}
|
||||
*/
|
||||
static findOrCreateByUrl(url) {
|
||||
return AssetModel.findOneAndUpdate({url}, {url}, {
|
||||
|
||||
// Ensure that if it's new, we return the new object created.
|
||||
new: true,
|
||||
|
||||
// Perform an upsert in the event that this doesn't exist.
|
||||
upsert: true,
|
||||
|
||||
// Set the default values if not provided based on the mongoose models.
|
||||
setDefaultsOnInsert: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the settings for the asset.
|
||||
* @param {[type]} id [description]
|
||||
* @param {[type]} settings [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
static overrideSettings(id, settings) {
|
||||
return AssetModel.findOneAndUpdate({id}, {
|
||||
$set: {
|
||||
settings
|
||||
}
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds assets matching keywords on the model. If `value` is an empty string,
|
||||
* then it will not even perform a text search query.
|
||||
* @param {String} value string to search by.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static search(value) {
|
||||
if (value.length === 0) {
|
||||
return AssetsService.all();
|
||||
} else {
|
||||
return AssetModel.find({
|
||||
$text: {
|
||||
$search: value
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds multiple assets with matching ids
|
||||
* @param {Array} ids an array of Strings of asset_id
|
||||
* @return {Promise} resolves to list of Assets
|
||||
*/
|
||||
static findMultipleById(ids) {
|
||||
const query = ids.map(id => ({id}));
|
||||
return AssetModel.find(query);
|
||||
}
|
||||
|
||||
static all() {
|
||||
return AssetModel.find({});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,220 @@
|
||||
const CommentModel = require('../models/comment');
|
||||
|
||||
const ActionModel = require('../models/action');
|
||||
const ActionsService = require('./actions');
|
||||
|
||||
module.exports = class CommentsService {
|
||||
|
||||
/**
|
||||
* Creates a new Comment that came from a public source.
|
||||
* @param {Mixed} comment either a single comment or an array of comments.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static publicCreate(comment) {
|
||||
|
||||
// Check to see if this is an array of comments, if so map it out.
|
||||
if (Array.isArray(comment)) {
|
||||
return Promise.all(comment.map(CommentsService.publicCreate));
|
||||
}
|
||||
|
||||
const {
|
||||
body,
|
||||
asset_id,
|
||||
parent_id,
|
||||
status = null,
|
||||
author_id
|
||||
} = comment;
|
||||
|
||||
comment = new CommentModel({
|
||||
body,
|
||||
asset_id,
|
||||
parent_id,
|
||||
status_history: status ? [{
|
||||
type: status,
|
||||
created_at: new Date()
|
||||
}] : [],
|
||||
status,
|
||||
author_id
|
||||
});
|
||||
|
||||
return comment.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a comment by the id.
|
||||
* @param {String} id identifier of comment (uuid)
|
||||
* @return {Promise}
|
||||
*/
|
||||
static findById(id) {
|
||||
return CommentModel.findOne({id});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds ALL the comments by the asset_id.
|
||||
* @param {String} asset_id identifier of the asset which owns this comment (uuid)
|
||||
* @return {Promise}
|
||||
*/
|
||||
static findByAssetId(asset_id) {
|
||||
return CommentModel.find({
|
||||
asset_id
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* findByAssetIdWithStatuses finds all the comments where the asset id matches
|
||||
* what's provided and the status is one of the ones listed in the statuses
|
||||
* array.
|
||||
* @param {String} asset_id the asset id to search by
|
||||
* @param {Array} [statuses=[]] the array of statuses to search by
|
||||
* @return {Promise} resolves to an array of comments
|
||||
*/
|
||||
static findByAssetIdWithStatuses(asset_id, statuses = []) {
|
||||
return CommentModel.find({
|
||||
asset_id,
|
||||
status: {
|
||||
$in: statuses
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
static findByActionType(action_type) {
|
||||
return ActionsService
|
||||
.findCommentsIdByActionType(action_type, 'COMMENTS')
|
||||
.then((actions) => CommentModel.find({
|
||||
id: {
|
||||
$in: actions.map((a) => a.item_id)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find comment id's where the action type matches the argument.
|
||||
* @param {String} action_type the type of action that was performed on the comment
|
||||
* @return {Promise}
|
||||
*/
|
||||
static findIdsByActionType(action_type) {
|
||||
return ActionsService
|
||||
.findCommentsIdByActionType(action_type, 'COMMENTS')
|
||||
.then((actions) => actions.map(a => a.item_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find comments by current status
|
||||
* @param {String} status status of the comment to search for
|
||||
* @return {Promise} resovles to comment array
|
||||
*/
|
||||
static findByStatus(status = null) {
|
||||
return CommentModel.find({status});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find comments that need to be moderated (aka moderation queue).
|
||||
* @param {String} asset_id
|
||||
* @return {Promise}
|
||||
*/
|
||||
static moderationQueue(status = null, asset_id = null) {
|
||||
|
||||
// Fetch the comments with statuses.
|
||||
let comments = CommentModel.find({status});
|
||||
|
||||
if (asset_id) {
|
||||
comments = comments.where({asset_id});
|
||||
}
|
||||
|
||||
return comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a new status in for the user.
|
||||
* @param {String} id identifier of the comment (uuid)
|
||||
* @param {String} status the new status of the comment
|
||||
* @param {String} assigned_by the user id for the user who performed the
|
||||
* moderation action
|
||||
* @return {Promise}
|
||||
*/
|
||||
static pushStatus(id, status, assigned_by = null) {
|
||||
return CommentModel.update({id}, {
|
||||
$push: {
|
||||
status_history: {
|
||||
type: status,
|
||||
created_at: new Date(),
|
||||
assigned_by
|
||||
}
|
||||
},
|
||||
$set: {status}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an action to the comment.
|
||||
* @param {String} item_id identifier of the comment (uuid)
|
||||
* @param {String} user_id user id of the action (uuid)
|
||||
* @param {String} action the new action to the comment
|
||||
* @return {Promise}
|
||||
*/
|
||||
static addAction(item_id, user_id, action_type, metadata) {
|
||||
return ActionsService.insertUserAction({
|
||||
item_id,
|
||||
item_type: 'COMMENTS',
|
||||
user_id,
|
||||
action_type,
|
||||
metadata
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
static removeById(id) {
|
||||
return CommentModel.remove({id});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an action from the comment.
|
||||
* @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}
|
||||
*/
|
||||
static removeAction(item_id, user_id, action_type) {
|
||||
return ActionModel.remove({
|
||||
action_type,
|
||||
item_type: 'COMMENTS',
|
||||
item_id,
|
||||
user_id
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the comments in the collection.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static all() {
|
||||
return CommentModel.find({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the comments by user
|
||||
* probably to be paginated at some point in the future
|
||||
* @return {Promise} array resolves to an array of comments by that user
|
||||
*/
|
||||
static findByUserId(author_id, admin = false) {
|
||||
|
||||
// do not return un-published comments for non-admins
|
||||
let query = {author_id};
|
||||
|
||||
if (!admin) {
|
||||
query.$nor = [{status: 'PREMOD'}, {status: 'REJECTED'}];
|
||||
}
|
||||
|
||||
return CommentModel.find(query);
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
const passport = require('passport');
|
||||
const User = require('../models/user');
|
||||
const Setting = require('../models/setting');
|
||||
const UsersService = require('./users');
|
||||
const SettingsService = require('./settings');
|
||||
const LocalStrategy = require('passport-local').Strategy;
|
||||
const FacebookStrategy = require('passport-facebook').Strategy;
|
||||
|
||||
@@ -13,7 +13,7 @@ passport.serializeUser((user, done) => {
|
||||
});
|
||||
|
||||
passport.deserializeUser((id, done) => {
|
||||
User
|
||||
UsersService
|
||||
.findById(id)
|
||||
.then((user) => {
|
||||
done(null, user);
|
||||
@@ -43,7 +43,7 @@ function ValidateUserLogin(loginProfile, user, done) {
|
||||
}
|
||||
|
||||
// The user is a local user, check if we need email confirmation.
|
||||
return Setting.retrieve().then(({requireEmailConfirmation = false}) => {
|
||||
return SettingsService.retrieve().then(({requireEmailConfirmation = false}) => {
|
||||
|
||||
// If we have the requirement of checking that emails for users are
|
||||
// verified, then we need to check the email address to ensure that it has
|
||||
@@ -77,7 +77,7 @@ passport.use(new LocalStrategy({
|
||||
usernameField: 'email',
|
||||
passwordField: 'password'
|
||||
}, (email, password, done) => {
|
||||
User
|
||||
UsersService
|
||||
.findLocalUser(email, password)
|
||||
.then((user) => {
|
||||
if (!user) {
|
||||
@@ -103,7 +103,7 @@ if (process.env.TALK_FACEBOOK_APP_ID && process.env.TALK_FACEBOOK_APP_SECRET &&
|
||||
callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`,
|
||||
profileFields: ['id', 'displayName', 'picture.type(large)']
|
||||
}, (accessToken, refreshToken, profile, done) => {
|
||||
User
|
||||
UsersService
|
||||
.findOrCreateExternalUser(profile)
|
||||
.then((user) => {
|
||||
return ValidateUserLogin(profile, user, done);
|
||||
|
||||
+4
-3
@@ -1,6 +1,7 @@
|
||||
const kue = require('./kue');
|
||||
const debug = require('debug')('talk:services:scraper');
|
||||
const Asset = require('../models/asset');
|
||||
const AssetModel = require('../models/asset');
|
||||
const AssetsService = require('./assets');
|
||||
|
||||
const metascraper = require('metascraper');
|
||||
|
||||
@@ -49,7 +50,7 @@ const scraper = {
|
||||
* Updates an Asset based on scraped asset metadata.
|
||||
*/
|
||||
update(id, meta) {
|
||||
return Asset.update({id}, {
|
||||
return AssetModel.update({id}, {
|
||||
$set: {
|
||||
title: meta.title || '',
|
||||
description: meta.description || '',
|
||||
@@ -74,7 +75,7 @@ const scraper = {
|
||||
|
||||
debug(`Starting on Job[${job.id}] for Asset[${job.data.asset_id}]`);
|
||||
|
||||
Asset
|
||||
AssetsService
|
||||
|
||||
// Find the asset, or complain that it doesn't exist.
|
||||
.findById(job.data.asset_id)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
const SettingModel = require('../models/setting');
|
||||
|
||||
/**
|
||||
* The selector used to uniquely identify the settings document.
|
||||
*/
|
||||
const selector = {id: '1'};
|
||||
|
||||
/**
|
||||
* The Setting Service object exposing the Setting model.
|
||||
*/
|
||||
module.exports = class SettingsService {
|
||||
|
||||
/**
|
||||
* Gets the entire settings record and sends it back
|
||||
* @return {Promise} settings the whole settings record
|
||||
*/
|
||||
static retrieve() {
|
||||
return SettingModel.findOne(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will update the settings object with whatever you pass in
|
||||
* @param {object} setting a hash of whatever settings you want to update
|
||||
* @return {Promise} settings Promise that resolves to the entire (updated) settings object.
|
||||
*/
|
||||
static update(settings) {
|
||||
return SettingModel.findOneAndUpdate(selector, {
|
||||
$set: settings
|
||||
}, {
|
||||
upsert: true,
|
||||
new: true,
|
||||
setDefaultsOnInsert: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is run once when the app starts to ensure settings are populated.
|
||||
* @return {Promise} null initialize the global settings object
|
||||
*/
|
||||
static init(defaults) {
|
||||
|
||||
// Inject the defaults on top of the passed in defaults to ensure that the new
|
||||
// settings conform to the required selector.
|
||||
defaults = Object.assign({}, defaults, selector);
|
||||
|
||||
// Actually update the settings collection.
|
||||
return SettingsService.update(defaults);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,601 @@
|
||||
const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Wordlist = require('./wordlist');
|
||||
const errors = require('../errors');
|
||||
const uuid = require('uuid');
|
||||
|
||||
const UserModel = require('../models/user');
|
||||
const USER_STATUS = require('../models/user').USER_STATUS;
|
||||
const USER_ROLES = require('../models/user').USER_ROLES;
|
||||
|
||||
const ActionsService = require('./actions');
|
||||
|
||||
// In the event that the TALK_SESSION_SECRET is missing but we are testing, then
|
||||
// set the process.env.TALK_SESSION_SECRET.
|
||||
if (process.env.NODE_ENV === 'test' && !process.env.TALK_SESSION_SECRET) {
|
||||
process.env.TALK_SESSION_SECRET = 'keyboard cat';
|
||||
} else if (!process.env.TALK_SESSION_SECRET) {
|
||||
throw new Error('TALK_SESSION_SECRET must be defined to encode JSON Web Tokens and other auth functionality');
|
||||
}
|
||||
|
||||
const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm';
|
||||
const PASSWORD_RESET_JWT_SUBJECT = 'password_reset';
|
||||
|
||||
// SALT_ROUNDS is the number of rounds that the bcrypt algorithm will run
|
||||
// through during the salting process.
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
// UsersService is the interface for the application to interact with the
|
||||
// UserModel through.
|
||||
module.exports = class UsersService {
|
||||
|
||||
/**
|
||||
* Finds a user given their email address that we have for them in the system
|
||||
* and ensures that the retuned user matches the password passed in as well.
|
||||
* @param {string} email - email to look up the user by
|
||||
* @param {string} password - password to match against the found user
|
||||
* @param {Function} done [description]
|
||||
*/
|
||||
static findLocalUser(email, password) {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return Promise.reject('email is required for findLocalUser');
|
||||
}
|
||||
|
||||
return UserModel.findOne({
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id: email.toLowerCase(),
|
||||
provider: 'local'
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((user) => {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
bcrypt.compare(password, user.password, (err, res) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
return resolve(user);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges two users together by taking all the profiles on a given user and
|
||||
* pushing them into the source user followed by deleting the destination user's
|
||||
* user account. This will not merge the roles associated with the source user.
|
||||
* @param {String} dstUserID id of the user to which is the target of the merge
|
||||
* @param {String} srcUserID id of the user to which is the source of the merge
|
||||
* @return {Promise} resolves when the users are merged
|
||||
*/
|
||||
static mergeUsers(dstUserID, srcUserID) {
|
||||
let srcUser, dstUser;
|
||||
|
||||
return Promise
|
||||
.all([
|
||||
UserModel.findOne({id: dstUserID}).exec(),
|
||||
UserModel.findOne({id: srcUserID}).exec()
|
||||
])
|
||||
.then((users) => {
|
||||
dstUser = users[0];
|
||||
srcUser = users[1];
|
||||
|
||||
srcUser.profiles.forEach((profile) => {
|
||||
dstUser.profiles.push(profile);
|
||||
});
|
||||
|
||||
return srcUser.remove();
|
||||
})
|
||||
.then(() => dstUser.save());
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a user given a social profile and if the user does not exist, creates
|
||||
* them.
|
||||
* @param {Object} profile - User social/external profile
|
||||
* @param {Function} done [description]
|
||||
*/
|
||||
static findOrCreateExternalUser(profile) {
|
||||
return UserModel
|
||||
.findOne({
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id: profile.id,
|
||||
provider: profile.provider
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((user) => {
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
|
||||
// The user was not found, lets create them!
|
||||
user = new UserModel({
|
||||
displayName: profile.displayName,
|
||||
roles: [],
|
||||
profiles: [
|
||||
{
|
||||
id: profile.id,
|
||||
provider: profile.provider
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return user.save();
|
||||
});
|
||||
}
|
||||
|
||||
static changePassword(id, password) {
|
||||
return new Promise((resolve, reject) => {
|
||||
bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve(hashedPassword);
|
||||
});
|
||||
})
|
||||
.then((hashedPassword) => {
|
||||
return UserModel.update({id}, {
|
||||
$inc: {__v: 1},
|
||||
$set: {
|
||||
password: hashedPassword
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates local users.
|
||||
* @param {Array} users Users to create
|
||||
* @return {Promise} Resolves with the users that were created
|
||||
*/
|
||||
static createLocalUsers(users) {
|
||||
return Promise.all(users.map((user) => {
|
||||
return UsersService
|
||||
.createLocalUser(user.email, user.password, user.displayName);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the requested displayname for naughty words (currently in English) and special chars
|
||||
* @param {String} displayName word to be checked for profanity
|
||||
* @return {Promise} rejected if the machine's sensibilites are offended
|
||||
*/
|
||||
static isValidDisplayName(displayName) {
|
||||
const onlyLettersNumbersUnderscore = /^[a-z0-9_]+$/;
|
||||
|
||||
if (!displayName) {
|
||||
return Promise.reject(errors.ErrMissingDisplay);
|
||||
}
|
||||
|
||||
if (!onlyLettersNumbersUnderscore.test(displayName)) {
|
||||
|
||||
return Promise.reject(errors.ErrSpecialChars);
|
||||
}
|
||||
|
||||
// check for profanity
|
||||
return Wordlist.displayNameCheck(displayName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the local user with a given email, password, and name.
|
||||
* @param {String} email email of the new user
|
||||
* @param {String} password plaintext password of the new user
|
||||
* @param {String} displayName name of the display user
|
||||
* @param {Function} done callback
|
||||
*/
|
||||
static createLocalUser(email, password, displayName) {
|
||||
|
||||
if (!email) {
|
||||
return Promise.reject(errors.ErrMissingEmail);
|
||||
}
|
||||
|
||||
email = email.toLowerCase().trim();
|
||||
displayName = displayName.toLowerCase().trim();
|
||||
|
||||
if (!password) {
|
||||
return Promise.reject(errors.ErrMissingPassword);
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return Promise.reject(errors.ErrPasswordTooShort);
|
||||
}
|
||||
|
||||
return UsersService.isValidDisplayName(displayName)
|
||||
.then(() => { // displayName is valid
|
||||
return new Promise((resolve, reject) => {
|
||||
bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
let user = new UserModel({
|
||||
displayName: displayName,
|
||||
password: hashedPassword,
|
||||
roles: [],
|
||||
profiles: [
|
||||
{
|
||||
id: email,
|
||||
provider: 'local'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
user.save((err) => {
|
||||
if (err) {
|
||||
if (err.code === 11000) {
|
||||
if (err.message.match('displayName')) {
|
||||
return reject(errors.ErrDisplayTaken);
|
||||
}
|
||||
return reject(errors.ErrEmailTaken);
|
||||
}
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(user);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables a given user account.
|
||||
* @param {String} id id of a user
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
static disableUser(id) {
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$set: {
|
||||
disabled: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables a given user account.
|
||||
* @param {String} id id of a user
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
static enableUser(id) {
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$set: {
|
||||
disabled: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a role to a user.
|
||||
* @param {String} id id of a user
|
||||
* @param {String} role role to add
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
static addRoleToUser(id, role) {
|
||||
|
||||
// Check to see if the user role is in the allowable set of roles.
|
||||
if (USER_ROLES.indexOf(role) === -1) {
|
||||
|
||||
// User role is not supported! Error out here.
|
||||
return Promise.reject(new Error(`role ${role} is not supported`));
|
||||
}
|
||||
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$addToSet: {
|
||||
roles: role
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a role from a user.
|
||||
* @param {String} id id of a user
|
||||
* @param {String} role role to remove
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
static removeRoleFromUser(id, role) {
|
||||
|
||||
// Check to see if the user role is in the allowable set of roles.
|
||||
if (USER_ROLES.indexOf(role) === -1) {
|
||||
|
||||
// User role is not supported! Error out here.
|
||||
return Promise.reject(new Error(`role ${role} is not supported`));
|
||||
}
|
||||
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$pull: {
|
||||
roles: role
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set status of a user.
|
||||
* @param {String} id id of a user
|
||||
* @param {String} status status to set
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
static setStatus(id, status) {
|
||||
|
||||
// Check to see if the user status is in the allowable set of roles.
|
||||
if (USER_STATUS.indexOf(status) === -1) {
|
||||
|
||||
// User status is not supported! Error out here.
|
||||
return Promise.reject(new Error(`status ${status} is not supported`));
|
||||
}
|
||||
|
||||
return UserModel.update({id}, {$set: {status}});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a user with the id.
|
||||
* @param {String} id user id (uuid)
|
||||
*/
|
||||
static findById(id) {
|
||||
return UserModel.findOne({id});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds users in an array of ids.
|
||||
* @param {Array} ids array of user identifiers (uuid)
|
||||
*/
|
||||
static findByIdArray(ids) {
|
||||
return UserModel.find({
|
||||
id: {$in: ids}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds public user information by an array of ids.
|
||||
* @param {Array} ids array of user identifiers (uuid)
|
||||
*/
|
||||
static findPublicByIdArray(ids) {
|
||||
return UserModel.find({
|
||||
id: {$in: ids}
|
||||
}, 'id displayName');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JWT from a user email. Only works for local accounts.
|
||||
* @param {String} email of the local user
|
||||
*/
|
||||
static createPasswordResetToken(email) {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return Promise.reject('email is required when creating a JWT for resetting passord');
|
||||
}
|
||||
|
||||
email = email.toLowerCase();
|
||||
|
||||
return UserModel.findOne({profiles: {$elemMatch: {id: email}}})
|
||||
.then((user) => {
|
||||
if (!user) {
|
||||
|
||||
// Since we don't want to reveal that the email does/doesn't exist
|
||||
// just go ahead and resolve the Promise with null and check in the
|
||||
// endpoint.
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
jti: uuid.v4(),
|
||||
email,
|
||||
userId: user.id,
|
||||
version: user.__v
|
||||
};
|
||||
|
||||
return jwt.sign(payload, process.env.TALK_SESSION_SECRET, {
|
||||
algorithm: 'HS256',
|
||||
expiresIn: '1d',
|
||||
subject: PASSWORD_RESET_JWT_SUBJECT
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the token was indeed signed by the session secret.
|
||||
* @param {String} token JWT token from the client
|
||||
* @return {Promise}
|
||||
*/
|
||||
static verifyToken(token, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// Set the allowed algorithms.
|
||||
options.algorithms = ['HS256'];
|
||||
|
||||
jwt.verify(token, process.env.TALK_SESSION_SECRET, options, (err, decoded) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve(decoded);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a jwt and returns the associated user.
|
||||
* @param {String} token the JSON Web Token to verify
|
||||
*/
|
||||
static verifyPasswordResetToken(token) {
|
||||
return UsersService
|
||||
.verifyToken(token, {
|
||||
subject: PASSWORD_RESET_JWT_SUBJECT
|
||||
})
|
||||
.then((decoded) => UsersService.findById(decoded.userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a user using a value which gets compared using a prefix match against
|
||||
* the user's email address and/or their display name.
|
||||
* @param {String} value value to search by
|
||||
* @return {Promise}
|
||||
*/
|
||||
static search(value) {
|
||||
return UserModel.find({
|
||||
$or: [
|
||||
|
||||
// Search by a prefix match on the displayName.
|
||||
{
|
||||
'displayName': {
|
||||
$regex: new RegExp(`^${value}`),
|
||||
$options: 'i'
|
||||
}
|
||||
},
|
||||
|
||||
// Search by a prefix match on the email address.
|
||||
{
|
||||
'profiles': {
|
||||
$elemMatch: {
|
||||
id: {
|
||||
$regex: new RegExp(`^${value}`),
|
||||
$options: 'i'
|
||||
},
|
||||
provider: 'local'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a count of the current users.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static count() {
|
||||
return UserModel.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the users.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static all() {
|
||||
UserModel.find();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the user's settings.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static updateSettings(id, settings) {
|
||||
return UserModel.update({
|
||||
id
|
||||
}, {
|
||||
$set: {
|
||||
settings
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an action to the user.
|
||||
* @param {String} item_id identifier of the user (uuid)
|
||||
* @param {String} user_id user id of the action (uuid)
|
||||
* @param {String} action the new action to the user
|
||||
* @return {Promise}
|
||||
*/
|
||||
static addAction(item_id, user_id, action_type, metadata) {
|
||||
return ActionsService.insertUserAction({
|
||||
item_id,
|
||||
item_type: 'users',
|
||||
user_id,
|
||||
action_type,
|
||||
metadata
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This creates a token based around confirming the local profile.
|
||||
* @param {String} userID The user id for the user that we are creating the
|
||||
* token for.
|
||||
* @param {String} email The email that we are needing to get confirmed.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static createEmailConfirmToken(userID, email) {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return Promise.reject('email is required when creating a JWT for resetting passord');
|
||||
}
|
||||
|
||||
email = email.toLowerCase();
|
||||
|
||||
return UsersService
|
||||
.findById(userID)
|
||||
.then((user) => {
|
||||
if (!user) {
|
||||
return Promise.reject(new Error('user not found'));
|
||||
}
|
||||
|
||||
// Get the profile representing the local account.
|
||||
let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local');
|
||||
|
||||
// Ensure that the user email hasn't already been verified.
|
||||
if (profile && profile.metadata && profile.metadata.confirmed_at) {
|
||||
return Promise.reject(new Error('email address already confirmed'));
|
||||
}
|
||||
|
||||
const payload = {
|
||||
email,
|
||||
userID
|
||||
};
|
||||
|
||||
return jwt.sign(payload, process.env.TALK_SESSION_SECRET, {
|
||||
jwtid: uuid.v4(),
|
||||
algorithm: 'HS256',
|
||||
expiresIn: '1d',
|
||||
subject: EMAIL_CONFIRM_JWT_SUBJECT
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This verifies that a given token was for the email confirmation and updates
|
||||
* that user's profile with a 'confirmed_at' parameter with the current date.
|
||||
* @param {String} token the token containing the email confirmation details
|
||||
* signed with our secret.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static verifyEmailConfirmation(token) {
|
||||
return UsersService
|
||||
.verifyToken(token, {
|
||||
subject: EMAIL_CONFIRM_JWT_SUBJECT
|
||||
})
|
||||
.then(({userID, email}) => {
|
||||
|
||||
return UserModel
|
||||
.update({
|
||||
id: userID,
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id: email,
|
||||
provider: 'local'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
$set: {
|
||||
'profiles.$.metadata.confirmed_at': new Date()
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
@@ -2,7 +2,7 @@ const debug = require('debug')('talk:services:wordlist');
|
||||
const _ = require('lodash');
|
||||
const natural = require('natural');
|
||||
const tokenizer = new natural.WordTokenizer();
|
||||
const Setting = require('../models/setting');
|
||||
const SettingsService = require('./settings');
|
||||
const Errors = require('../errors');
|
||||
|
||||
/**
|
||||
@@ -22,7 +22,7 @@ class Wordlist {
|
||||
* Loads wordlists in from the database
|
||||
*/
|
||||
load() {
|
||||
return Setting
|
||||
return SettingsService
|
||||
.retrieve()
|
||||
.then((settings) => {
|
||||
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
const Comment = require('../../models/comment');
|
||||
const User = require('../../models/user');
|
||||
const Action = require('../../models/action');
|
||||
const Setting = require('../../models/setting');
|
||||
|
||||
const settings = {id: '1', moderation: 'pre', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
|
||||
|
||||
const expect = require('chai').expect;
|
||||
|
||||
describe('models.Comment', () => {
|
||||
const comments = [{
|
||||
body: 'comment 10',
|
||||
asset_id: '123',
|
||||
status_history: [],
|
||||
parent_id: '',
|
||||
author_id: '123',
|
||||
id: '1'
|
||||
}, {
|
||||
body: 'comment 20',
|
||||
asset_id: '123',
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}],
|
||||
status: 'accepted',
|
||||
parent_id: '',
|
||||
author_id: '123',
|
||||
id: '2'
|
||||
}, {
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status_history: [],
|
||||
parent_id: '',
|
||||
author_id: '456',
|
||||
id: '3'
|
||||
}, {
|
||||
body: 'comment 40',
|
||||
asset_id: '123',
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}],
|
||||
status: 'rejected',
|
||||
parent_id: '',
|
||||
author_id: '456',
|
||||
id: '4'
|
||||
}, {
|
||||
body: 'comment 50',
|
||||
asset_id: '1234',
|
||||
status_history: [{
|
||||
type: 'premod'
|
||||
}],
|
||||
status: 'premod',
|
||||
parent_id: '',
|
||||
author_id: '456',
|
||||
id: '5'
|
||||
}, {
|
||||
body: 'comment 60',
|
||||
asset_id: '1234',
|
||||
status_history: [{
|
||||
type: 'premod'
|
||||
}],
|
||||
status: 'premod',
|
||||
parent_id: '',
|
||||
author_id: '456',
|
||||
id: '6'
|
||||
}];
|
||||
|
||||
const users = [{
|
||||
email: 'stampi@gmail.com',
|
||||
displayName: 'Stampi',
|
||||
password: '1Coral!!'
|
||||
}, {
|
||||
email: 'sockmonster@gmail.com',
|
||||
displayName: 'Sockmonster',
|
||||
password: '2Coral!!'
|
||||
}];
|
||||
|
||||
const actions = [{
|
||||
action_type: 'flag',
|
||||
item_id: '3',
|
||||
item_type: 'comments',
|
||||
user_id: '123'
|
||||
}, {
|
||||
action_type: 'like',
|
||||
item_id: '1',
|
||||
item_type: 'comments',
|
||||
user_id: '456'
|
||||
}];
|
||||
|
||||
beforeEach(() => {
|
||||
return Setting.init(settings).then(() => {
|
||||
return Promise.all([
|
||||
Comment.create(comments),
|
||||
User.createLocalUsers(users),
|
||||
Action.create(actions)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#publicCreate()', () => {
|
||||
|
||||
it('creates a new comment', () => {
|
||||
return Comment.publicCreate({
|
||||
body: 'This is a comment!',
|
||||
status: 'accepted'
|
||||
}).then((c) => {
|
||||
expect(c).to.not.be.null;
|
||||
expect(c.id).to.not.be.null;
|
||||
expect(c.id).to.be.uuid;
|
||||
expect(c.status).to.be.equal('accepted');
|
||||
});
|
||||
});
|
||||
|
||||
it('creates many new comments', () => {
|
||||
return Comment.publicCreate([{
|
||||
body: 'This is a comment!',
|
||||
status: 'accepted'
|
||||
}, {
|
||||
body: 'This is another comment!'
|
||||
}, {
|
||||
body: 'This is a rejected comment!',
|
||||
status: 'rejected'
|
||||
}]).then(([c1, c2, c3]) => {
|
||||
expect(c1).to.not.be.null;
|
||||
expect(c1.id).to.be.uuid;
|
||||
expect(c1.status).to.be.equal('accepted');
|
||||
|
||||
expect(c2).to.not.be.null;
|
||||
expect(c2.id).to.be.uuid;
|
||||
expect(c2.status).to.be.null;
|
||||
|
||||
expect(c3).to.not.be.null;
|
||||
expect(c3.id).to.be.uuid;
|
||||
expect(c3.status).to.be.equal('rejected');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#findById()', () => {
|
||||
|
||||
it('should find a comment by id', () => {
|
||||
return Comment.findById('1').then((result) => {
|
||||
expect(result).to.not.be.null;
|
||||
expect(result).to.have.property('body', 'comment 10');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#findByAssetId()', () => {
|
||||
|
||||
it('should find an array of all comments by asset id', () => {
|
||||
return Comment.findByAssetId('123').then((result) => {
|
||||
expect(result).to.have.length(3);
|
||||
result.sort((a, b) => {
|
||||
if (a.body < b.body) {return -1;}
|
||||
else {return 1;}
|
||||
});
|
||||
expect(result[0]).to.have.property('body', 'comment 10');
|
||||
expect(result[1]).to.have.property('body', 'comment 20');
|
||||
expect(result[2]).to.have.property('body', 'comment 40');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#moderationQueue()', () => {
|
||||
|
||||
it('should find an array of new comments to moderate when pre-moderation', () => {
|
||||
return Comment.moderationQueue('premod').then((result) => {
|
||||
expect(result).to.not.be.null;
|
||||
expect(result).to.have.lengthOf(2);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#removeAction', () => {
|
||||
|
||||
it('should remove an action', () => {
|
||||
return Comment.removeAction('3', '123', 'flag')
|
||||
.then(() => {
|
||||
return Action.findByItemIdArray(['123']);
|
||||
})
|
||||
.then((actions) => {
|
||||
expect(actions.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#findByUserId', () => {
|
||||
it('should return all comments if admin', () => {
|
||||
return Comment.findByUserId('456', true)
|
||||
.then(comments => {
|
||||
expect(comments).to.have.length(4);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return premod and rejected comments if not admin', () => {
|
||||
return Comment.findByUserId('456')
|
||||
.then(comments => {
|
||||
expect(comments).to.have.length(1);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#changeStatus', () => {
|
||||
|
||||
it('should change the status of a comment from no status', () => {
|
||||
let comment_id = comments[0].id;
|
||||
|
||||
return Comment.findById(comment_id)
|
||||
.then((c) => {
|
||||
expect(c.status).to.be.null;
|
||||
|
||||
return Comment.pushStatus(comment_id, 'rejected', '123');
|
||||
})
|
||||
.then(() => Comment.findById(comment_id))
|
||||
.then((c) => {
|
||||
expect(c).to.have.property('status');
|
||||
expect(c.status).to.equal('rejected');
|
||||
expect(c.status_history).to.have.length(1);
|
||||
expect(c.status_history[0]).to.have.property('type', 'rejected');
|
||||
expect(c.status_history[0]).to.have.property('assigned_by', '123');
|
||||
});
|
||||
});
|
||||
|
||||
it('should change the status of a comment from accepted', () => {
|
||||
return Comment.pushStatus(comments[1].id, 'rejected', '123')
|
||||
.then(() => Comment.findById(comments[1].id))
|
||||
.then((c) => {
|
||||
expect(c).to.have.property('status_history');
|
||||
expect(c).to.have.property('status');
|
||||
expect(c.status).to.equal('rejected');
|
||||
expect(c.status_history).to.have.length(2);
|
||||
expect(c.status_history[0]).to.have.property('type', 'accepted');
|
||||
expect(c.status_history[0]).to.have.property('assigned_by', null);
|
||||
|
||||
expect(c.status_history[1]).to.have.property('type', 'rejected');
|
||||
expect(c.status_history[1]).to.have.property('assigned_by', '123');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -8,12 +8,13 @@ const expect = chai.expect;
|
||||
chai.should();
|
||||
chai.use(require('chai-http'));
|
||||
|
||||
const Asset = require('../../../../models/asset');
|
||||
const AssetModel = require('../../../../models/asset');
|
||||
const AssetsService = require('../../../../services/assets');
|
||||
|
||||
describe('/api/v1/assets', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
return Asset.create([
|
||||
return AssetModel.create([
|
||||
{
|
||||
url: 'https://coralproject.net/news/asset1',
|
||||
title: 'Asset 1',
|
||||
@@ -121,7 +122,7 @@ describe('/api/v1/assets', () => {
|
||||
|
||||
const today = Date.now();
|
||||
|
||||
return Asset.findOrCreateByUrl('http://test.com')
|
||||
return AssetsService.findOrCreateByUrl('http://test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('isClosed', null);
|
||||
expect(asset).to.have.property('closedAt', null);
|
||||
@@ -135,7 +136,7 @@ describe('/api/v1/assets', () => {
|
||||
|
||||
expect(res).to.have.status(204);
|
||||
|
||||
return Asset.findByUrl('http://test.com');
|
||||
return AssetsService.findByUrl('http://test.com');
|
||||
})
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('isClosed', true);
|
||||
|
||||
@@ -4,7 +4,7 @@ const expect = chai.expect;
|
||||
|
||||
chai.use(require('chai-http'));
|
||||
|
||||
const User = require('../../../../models/user');
|
||||
const UsersService = require('../../../../services/users');
|
||||
|
||||
describe('/api/v1/auth', () => {
|
||||
describe('#get', () => {
|
||||
@@ -19,15 +19,15 @@ describe('/api/v1/auth', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const Setting = require('../../../../models/setting');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
|
||||
describe('/api/v1/auth/local', () => {
|
||||
|
||||
let mockUser;
|
||||
beforeEach(() => {
|
||||
const settings = {requireEmailConfirmation: false, wordlist: {banned: ['bad'], suspect: ['naughty']}};
|
||||
return Setting.init(settings).then(() => {
|
||||
return User.createLocalUser('maria@gmail.com', 'password!', 'Maria')
|
||||
return SettingsService.init(settings).then(() => {
|
||||
return UsersService.createLocalUser('maria@gmail.com', 'password!', 'Maria')
|
||||
.then((user) => {
|
||||
mockUser = user;
|
||||
});
|
||||
@@ -66,7 +66,7 @@ describe('/api/v1/auth/local', () => {
|
||||
|
||||
describe('email confirmation enabled', () => {
|
||||
|
||||
beforeEach(() => Setting.init({requireEmailConfirmation: true}));
|
||||
beforeEach(() => SettingsService.init({requireEmailConfirmation: true}));
|
||||
|
||||
describe('#post', () => {
|
||||
it('should not allow a login from a user that is not confirmed', () => {
|
||||
@@ -76,9 +76,9 @@ describe('/api/v1/auth/local', () => {
|
||||
.catch((err) => {
|
||||
err.response.should.have.status(401);
|
||||
|
||||
return User.createEmailConfirmToken(mockUser.id, mockUser.profiles[0].id);
|
||||
return UsersService.createEmailConfirmToken(mockUser.id, mockUser.profiles[0].id);
|
||||
})
|
||||
.then(User.verifyEmailConfirmation)
|
||||
.then(UsersService.verifyEmailConfirmation)
|
||||
.then(() => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/auth/local')
|
||||
|
||||
@@ -8,18 +8,19 @@ const expect = chai.expect;
|
||||
chai.should();
|
||||
chai.use(require('chai-http'));
|
||||
|
||||
const Comment = require('../../../../models/comment');
|
||||
const Asset = require('../../../../models/asset');
|
||||
const Action = require('../../../../models/action');
|
||||
const User = require('../../../../models/user');
|
||||
const CommentModel = require('../../../../models/comment');
|
||||
const ActionModel = require('../../../../models/action');
|
||||
|
||||
const CommentsService = require('../../../../services/comments');
|
||||
const UsersService = require('../../../../services/users');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
|
||||
const Setting = require('../../../../models/setting');
|
||||
const settings = {id: '1', moderation: 'pre', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
|
||||
|
||||
describe('/api/v1/comments', () => {
|
||||
|
||||
// Ensure that the settings are always available.
|
||||
beforeEach(() => Setting.init(settings));
|
||||
beforeEach(() => SettingsService.init(settings));
|
||||
|
||||
describe('#get', () => {
|
||||
const comments = [{
|
||||
@@ -34,16 +35,16 @@ describe('/api/v1/comments', () => {
|
||||
body: 'comment 20',
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
status: 'rejected',
|
||||
status: 'REJECTED',
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
type: 'REJECTED'
|
||||
}]
|
||||
}, {
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status: 'accepted',
|
||||
status: 'ACCEPTED',
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
type: 'ACCEPTED'
|
||||
}]
|
||||
}];
|
||||
|
||||
@@ -58,18 +59,18 @@ describe('/api/v1/comments', () => {
|
||||
}];
|
||||
|
||||
const actions = [{
|
||||
action_type: 'flag',
|
||||
action_type: 'FLAG',
|
||||
item_id: 'abc',
|
||||
item_type: 'comments'
|
||||
item_type: 'COMMENTS'
|
||||
}, {
|
||||
action_type: 'like',
|
||||
action_type: 'LIKE',
|
||||
item_id: 'hij',
|
||||
item_type: 'comments'
|
||||
item_type: 'COMMENTS'
|
||||
}];
|
||||
|
||||
beforeEach(() => {
|
||||
return Promise.all([
|
||||
Comment.create(comments).then((newComments) => {
|
||||
CommentModel.create(comments).then((newComments) => {
|
||||
newComments.forEach((comment, i) => {
|
||||
comments[i].id = comment.id;
|
||||
});
|
||||
@@ -77,9 +78,9 @@ describe('/api/v1/comments', () => {
|
||||
actions[0].item_id = comments[0].id;
|
||||
actions[1].item_id = comments[1].id;
|
||||
|
||||
return Action.create(actions);
|
||||
return ActionModel.create(actions);
|
||||
}),
|
||||
User.createLocalUsers(users)
|
||||
UsersService.createLocalUsers(users)
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -119,7 +120,7 @@ describe('/api/v1/comments', () => {
|
||||
|
||||
it('should return all the rejected comments', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/comments?status=rejected')
|
||||
.get('/api/v1/comments?status=REJECTED')
|
||||
.set(passport.inject({roles: ['admin']}))
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
@@ -131,7 +132,7 @@ describe('/api/v1/comments', () => {
|
||||
|
||||
it('should return all the approved comments', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/comments?status=accepted')
|
||||
.get('/api/v1/comments?status=ACCEPTED')
|
||||
.set(passport.inject({roles: ['admin']}))
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
@@ -142,7 +143,7 @@ describe('/api/v1/comments', () => {
|
||||
|
||||
it('should return all the new comments', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/comments?status=new')
|
||||
.get('/api/v1/comments?status=NEW')
|
||||
.set(passport.inject({roles: ['admin']}))
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
@@ -152,7 +153,7 @@ describe('/api/v1/comments', () => {
|
||||
|
||||
it('should return all the flagged comments', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/comments?action_type=flag')
|
||||
.get('/api/v1/comments?action_type=FLAG')
|
||||
.set(passport.inject({roles: ['admin']}))
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
@@ -162,174 +163,6 @@ describe('/api/v1/comments', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#post', () => {
|
||||
|
||||
let asset_id;
|
||||
let postmod_asset_id;
|
||||
|
||||
beforeEach(() => Promise.all([
|
||||
Asset.findOrCreateByUrl('https://coralproject.net/section/article-is-the-best').then((asset) => {
|
||||
|
||||
// Update the asset id.
|
||||
asset_id = asset.id;
|
||||
}),
|
||||
Asset.findOrCreateByUrl('https://coralproject.net/section/postmod-article-is-the-best').then((asset) => {
|
||||
|
||||
// Update the asset id.
|
||||
postmod_asset_id = asset.id;
|
||||
|
||||
return Asset.overrideSettings(postmod_asset_id, {moderation: 'post'});
|
||||
}),
|
||||
]));
|
||||
|
||||
it('should create a comment', () => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset_id, 'parent_id': ''})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res.body).to.have.property('id');
|
||||
expect(res.body).to.have.property('status', 'premod');
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a comment with a rejected status if it contains a bad word', () => {
|
||||
return chai.request(app).post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'bad words are the baddest', 'author_id': '123', 'asset_id': asset_id, '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');
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a comment with no status and a flag if it contains a suspected word', () => {
|
||||
return chai.request(app).post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'suspect words are the most suspicious', 'author_id': '123', 'asset_id': postmod_asset_id, 'parent_id': ''})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res.body).to.have.property('id');
|
||||
expect(res.body).to.have.property('status', null);
|
||||
|
||||
return Promise.all([
|
||||
res.body,
|
||||
Action.findByType('flag', 'comments')
|
||||
]);
|
||||
})
|
||||
.then(([comment, actions]) => {
|
||||
expect(actions).to.have.length(1);
|
||||
|
||||
let action = actions[0];
|
||||
|
||||
expect(action).to.have.property('item_id', comment.id);
|
||||
expect(action).to.have.property('metadata');
|
||||
expect(action.metadata).to.have.property('field', 'body');
|
||||
expect(action.metadata).to.have.property('details', 'Matched suspect word filters.');
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a comment with a premod status if it\'s asset is has pre-moderation enabled', () => {
|
||||
return Asset
|
||||
.findOrCreateByUrl('https://coralproject.net/article1')
|
||||
.then((asset) => {
|
||||
return Asset
|
||||
.overrideSettings(asset.id, {moderation: 'pre'})
|
||||
.then(() => asset);
|
||||
})
|
||||
.then((asset) => {
|
||||
return chai.request(app).post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res.body).to.have.property('id');
|
||||
expect(res.body).to.have.property('asset_id');
|
||||
expect(res.body).to.have.property('status', 'premod');
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a comment with null status if it\'s asset is has post-moderation enabled', () => {
|
||||
return Asset
|
||||
.findOrCreateByUrl('https://coralproject.net/article1')
|
||||
.then((asset) => {
|
||||
return Asset
|
||||
.overrideSettings(asset.id, {moderation: 'post'})
|
||||
.then(() => asset);
|
||||
})
|
||||
.then((asset) => {
|
||||
return chai.request(app).post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res.body).to.have.property('id');
|
||||
expect(res.body).to.have.property('asset_id');
|
||||
expect(res.body).to.have.property('status', null);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a rejected comment if the body is above the character count', () => {
|
||||
return Asset
|
||||
.findOrCreateByUrl('https://coralproject.net/article1')
|
||||
.then((asset) => {
|
||||
return Asset
|
||||
.overrideSettings(asset.id, {charCountEnable: true, charCount: 10})
|
||||
.then(() => asset);
|
||||
})
|
||||
.then((asset) => {
|
||||
return chai.request(app).post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'This is way way way way way too long.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res.body).to.have.property('id');
|
||||
expect(res.body).to.have.property('asset_id');
|
||||
expect(res.body).to.have.property('status', 'rejected');
|
||||
});
|
||||
});
|
||||
|
||||
it('shouldn\'t create a comment when the asset has expired commenting', () => {
|
||||
return Asset.create({
|
||||
closedAt: new Date().setDate(0),
|
||||
closedMessage: 'tests said expired!'
|
||||
})
|
||||
.then((asset) => {
|
||||
return chai.request(app).post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(500);
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err.response.body).to.not.be.null;
|
||||
expect(err.response.body).to.have.property('message');
|
||||
expect(err.response.body.error.metadata.closedMessage).to.be.equal('tests said expired!');
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a comment when the asset has not expired yet', () => {
|
||||
return Asset.create({
|
||||
closedAt: new Date().setDate(32),
|
||||
closedMessage: 'tests said expired!'
|
||||
})
|
||||
.then((asset) => {
|
||||
return chai.request(app).post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/api/v1/comments/:comment_id', () => {
|
||||
@@ -360,21 +193,21 @@ describe('/api/v1/comments/:comment_id', () => {
|
||||
}];
|
||||
|
||||
const actions = [{
|
||||
action_type: 'flag',
|
||||
action_type: 'FLAG',
|
||||
item_id: 'abc',
|
||||
item_type: 'comment'
|
||||
item_type: 'COMMENTS'
|
||||
}, {
|
||||
action_type: 'like',
|
||||
action_type: 'LIKE',
|
||||
item_id: 'hij',
|
||||
item_type: 'comment'
|
||||
item_type: 'COMMENTS'
|
||||
}];
|
||||
|
||||
beforeEach(() => {
|
||||
return Setting.init(settings).then(() => {
|
||||
return SettingsService.init(settings).then(() => {
|
||||
return Promise.all([
|
||||
Comment.create(comments),
|
||||
User.createLocalUsers(users),
|
||||
Action.create(actions)
|
||||
CommentModel.create(comments),
|
||||
UsersService.createLocalUsers(users),
|
||||
ActionModel.create(actions)
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -400,7 +233,7 @@ describe('/api/v1/comments/:comment_id', () => {
|
||||
.set(passport.inject({roles: ['admin']}))
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(204);
|
||||
return Comment.findById('abc');
|
||||
return CommentsService.findById('abc');
|
||||
})
|
||||
.then((comment) => {
|
||||
expect(comment).to.be.null;
|
||||
@@ -448,15 +281,17 @@ describe('/api/v1/comments/:comment_id/actions', () => {
|
||||
body: 'comment 20',
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
status: 'REJECTED',
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
type: 'REJECTED'
|
||||
}]
|
||||
}, {
|
||||
id: 'hij',
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status: 'ACCEPTED',
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
type: 'ACCEPTED'
|
||||
}]
|
||||
}];
|
||||
|
||||
@@ -471,19 +306,21 @@ describe('/api/v1/comments/:comment_id/actions', () => {
|
||||
}];
|
||||
|
||||
const actions = [{
|
||||
action_type: 'flag',
|
||||
action_type: 'FLAG',
|
||||
item_type: 'COMMENTS',
|
||||
item_id: 'abc'
|
||||
}, {
|
||||
action_type: 'like',
|
||||
action_type: 'LIKE',
|
||||
item_type: 'COMMENTS',
|
||||
item_id: 'hij'
|
||||
}];
|
||||
|
||||
beforeEach(() => {
|
||||
return Setting.init(settings).then(() => {
|
||||
return SettingsService.init(settings).then(() => {
|
||||
return Promise.all([
|
||||
Comment.create(comments),
|
||||
User.createLocalUsers(users),
|
||||
Action.create(actions)
|
||||
CommentModel.create(comments),
|
||||
UsersService.createLocalUsers(users),
|
||||
ActionModel.create(actions)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,9 +10,9 @@ chai.use(require('chai-http'));
|
||||
|
||||
const Comment = require('../../../../models/comment');
|
||||
const Action = require('../../../../models/action');
|
||||
const User = require('../../../../models/user');
|
||||
const UsersService = require('../../../../services/users');
|
||||
|
||||
const Setting = require('../../../../models/setting');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const settings = {id: '1', moderation: 'pre', wordlist: {banned: ['banned'], suspect: ['suspect']}};
|
||||
|
||||
describe('/api/v1/queue', () => {
|
||||
@@ -21,26 +21,26 @@ describe('/api/v1/queue', () => {
|
||||
body: 'comment 10',
|
||||
asset_id: 'asset',
|
||||
author_id: '123',
|
||||
status: 'rejected',
|
||||
status: 'REJECTED',
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
type: 'REJECTED'
|
||||
}]
|
||||
}, {
|
||||
id: 'def',
|
||||
body: 'comment 20',
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
status: 'premod',
|
||||
status: 'PREMOD',
|
||||
status_history: [{
|
||||
type: 'premod'
|
||||
type: 'PREMOD'
|
||||
}]
|
||||
}, {
|
||||
id: 'hij',
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status: 'accepted',
|
||||
status: 'ACCEPTED',
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
type: 'ACCEPTED'
|
||||
}]
|
||||
}];
|
||||
|
||||
@@ -55,18 +55,18 @@ describe('/api/v1/queue', () => {
|
||||
}];
|
||||
|
||||
const actions = [{
|
||||
action_type: 'flag',
|
||||
action_type: 'FLAG',
|
||||
item_id: 'abc',
|
||||
item_type: 'comment'
|
||||
item_type: 'COMMENTS'
|
||||
}, {
|
||||
action_type: 'like',
|
||||
action_type: 'LIKE',
|
||||
item_id: 'hij',
|
||||
item_type: 'comment'
|
||||
item_type: 'COMMENTS'
|
||||
}];
|
||||
|
||||
beforeEach(() => {
|
||||
return Setting.init(settings).then(() => {
|
||||
return User.createLocalUsers(users)
|
||||
return SettingsService.init(settings).then(() => {
|
||||
return UsersService.createLocalUsers(users)
|
||||
.then((u) => {
|
||||
comments[0].author_id = u[0].id;
|
||||
comments[1].author_id = u[1].id;
|
||||
@@ -80,7 +80,7 @@ describe('/api/v1/queue', () => {
|
||||
|
||||
return Promise.all([
|
||||
Action.create(actions),
|
||||
Setting.init(settings)
|
||||
SettingsService.init(settings)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,12 +7,12 @@ const expect = chai.expect;
|
||||
chai.should();
|
||||
chai.use(require('chai-http'));
|
||||
|
||||
const Setting = require('../../../../models/setting');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const defaults = {id: '1', moderation: 'pre'};
|
||||
|
||||
describe('/api/v1/settings', () => {
|
||||
|
||||
beforeEach(() => Setting.init(defaults));
|
||||
beforeEach(() => SettingsService.init(defaults));
|
||||
|
||||
describe('#get', () => {
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('/api/v1/settings', () => {
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(204);
|
||||
|
||||
return Setting.retrieve();
|
||||
return SettingsService.retrieve();
|
||||
})
|
||||
.then((settings) => {
|
||||
expect(settings).to.have.property('moderation', 'post');
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
const app = require('../../../../app');
|
||||
const chai = require('chai');
|
||||
const expect = chai.expect;
|
||||
|
||||
// Setup chai.
|
||||
chai.should();
|
||||
chai.use(require('chai-http'));
|
||||
|
||||
const Action = require('../../../../models/action');
|
||||
const User = require('../../../../models/user');
|
||||
const Comment = require('../../../../models/comment');
|
||||
const Asset = require('../../../../models/asset');
|
||||
|
||||
const Setting = require('../../../../models/setting');
|
||||
|
||||
describe('/api/v1/stream', () => {
|
||||
describe('#get', () => {
|
||||
const settings = {
|
||||
id: '1',
|
||||
moderation: 'post',
|
||||
wordlist: {
|
||||
banned: ['banned'],
|
||||
suspect: ['suspect']
|
||||
}
|
||||
};
|
||||
|
||||
const assets = [
|
||||
{
|
||||
url: 'https://example.com/article/1'
|
||||
},
|
||||
{
|
||||
url: 'https://example.com/article/2',
|
||||
settings: {
|
||||
moderation: 'pre'
|
||||
}
|
||||
},
|
||||
{
|
||||
url: 'https://example.com/article/3'
|
||||
}
|
||||
];
|
||||
|
||||
const comments = [{
|
||||
id: 'abc',
|
||||
body: 'comment 10',
|
||||
author_id: '',
|
||||
parent_id: '',
|
||||
status: 'accepted',
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
}, {
|
||||
id: 'def',
|
||||
body: 'comment 20',
|
||||
author_id: '',
|
||||
parent_id: '',
|
||||
status: null,
|
||||
status_history: []
|
||||
}, {
|
||||
id: 'uio',
|
||||
body: 'comment 30',
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
parent_id: '',
|
||||
status: 'accepted',
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
}, {
|
||||
id: 'hij',
|
||||
body: 'comment 40',
|
||||
asset_id: '456',
|
||||
status: 'rejected',
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}]
|
||||
}, {
|
||||
body: 'comment 50',
|
||||
status: 'premod',
|
||||
status_history: [{
|
||||
type: 'premod'
|
||||
}]
|
||||
}, {
|
||||
body: 'comment 60',
|
||||
status: 'accepted',
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
}, {
|
||||
body: 'comment 70',
|
||||
status: 'rejected',
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}]
|
||||
}, {
|
||||
body: 'comment 70',
|
||||
status: null,
|
||||
status_history: []
|
||||
}];
|
||||
|
||||
const users = [{
|
||||
displayName: 'Ana',
|
||||
email: 'ana@gmail.com',
|
||||
password: '123456789'
|
||||
}, {
|
||||
displayName: 'Maria',
|
||||
email: 'maria@gmail.com',
|
||||
password: '123456789'
|
||||
}];
|
||||
|
||||
const actions = [{
|
||||
action_type: 'flag',
|
||||
item_id: 'abc'
|
||||
}, {
|
||||
action_type: 'like',
|
||||
item_id: 'hij'
|
||||
}];
|
||||
|
||||
beforeEach(() => {
|
||||
return Setting.init(settings)
|
||||
.then(() => Promise.all([
|
||||
User.createLocalUsers(users),
|
||||
Promise.all(assets.map((asset) => Asset.create(asset)))
|
||||
]))
|
||||
.then(([mockUsers, mockAssets]) => {
|
||||
|
||||
// Map the id's over.
|
||||
mockAssets.forEach((asset, i) => {
|
||||
assets[i].id = asset.id;
|
||||
});
|
||||
|
||||
mockUsers.forEach((user, i) => {
|
||||
users[i].id = user.id;
|
||||
});
|
||||
|
||||
comments.forEach((comment, i) => {
|
||||
comments[i].author_id = users[(i % 2) === 0 ? 0 : 1].id;
|
||||
});
|
||||
|
||||
comments[0].asset_id = assets[0].id;
|
||||
comments[1].asset_id = assets[0].id;
|
||||
comments[2].asset_id = assets[1].id;
|
||||
comments[3].asset_id = assets[1].id;
|
||||
comments[4].asset_id = assets[2].id;
|
||||
comments[5].asset_id = assets[2].id;
|
||||
comments[6].asset_id = assets[2].id;
|
||||
comments[7].asset_id = assets[2].id;
|
||||
|
||||
return Promise.all([
|
||||
Comment.create(comments),
|
||||
Action.create(actions)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a stream with comments, users and actions for an existing asset', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/stream')
|
||||
.query({asset_url: assets[0].url})
|
||||
.then(res => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body.assets.length).to.equal(1);
|
||||
expect(res.body.comments.length).to.equal(2);
|
||||
expect(res.body.users.length).to.equal(2);
|
||||
expect(res.body.actions.length).to.equal(1);
|
||||
expect(res.body.settings).to.have.property('moderation', 'post');
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject requests without a scheme in the asset_url', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/stream')
|
||||
.query({asset_url: 'test.com'})
|
||||
.catch((err) => {
|
||||
expect(err).to.have.status(400);
|
||||
expect(err.response.body.message).to.contain('asset_url is invalid');
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge the settings when the asset contains settings to override it with', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/stream')
|
||||
.query({asset_url: assets[1].url})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body.assets).to.have.length(1);
|
||||
expect(res.body.comments).to.have.length(1);
|
||||
expect(res.body.users).to.have.length(1);
|
||||
expect(res.body.settings).to.have.property('moderation', 'pre');
|
||||
expect(res.body.settings).to.not.have.property('wordlist');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not change the previously displayed comments based on moderation state changes', () => {
|
||||
|
||||
let preComments, postComments;
|
||||
|
||||
return chai.request(app)
|
||||
.get('/api/v1/stream')
|
||||
.query({asset_url: assets[2].url})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body.comments.length).to.equal(2);
|
||||
expect(res.body.settings).to.have.property('moderation', 'post');
|
||||
|
||||
preComments = res.body.comments;
|
||||
|
||||
return Asset.overrideSettings(assets[2].id, {moderation: 'pre'});
|
||||
})
|
||||
.then(() => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/stream')
|
||||
.query({asset_url: assets[2].url});
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body.comments.length).to.equal(2);
|
||||
expect(res.body.settings).to.have.property('moderation', 'pre');
|
||||
|
||||
postComments = res.body.comments;
|
||||
|
||||
expect(preComments).to.deep.equal(postComments);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,21 +5,21 @@ const mailer = require('../../../../services/mailer');
|
||||
const chai = require('chai');
|
||||
const expect = chai.expect;
|
||||
|
||||
const Setting = require('../../../../models/setting');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const settings = {id: '1', moderation: 'pre', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
|
||||
|
||||
// Setup chai.
|
||||
chai.should();
|
||||
chai.use(require('chai-http'));
|
||||
|
||||
const User = require('../../../../models/user');
|
||||
const UsersService = require('../../../../services/users');
|
||||
|
||||
describe('/api/v1/users/:user_id/email/confirm', () => {
|
||||
|
||||
let mockUser;
|
||||
|
||||
beforeEach(() => Setting.init(settings).then(() => {
|
||||
return User.createLocalUser('ana@gmail.com', '123321123', 'Ana');
|
||||
beforeEach(() => SettingsService.init(settings).then(() => {
|
||||
return UsersService.createLocalUser('ana@gmail.com', '123321123', 'Ana');
|
||||
})
|
||||
.then((user) => {
|
||||
mockUser = user;
|
||||
@@ -63,8 +63,8 @@ describe('/api/v1/users/:user_id/actions', () => {
|
||||
}];
|
||||
|
||||
beforeEach(() => {
|
||||
return Setting.init(settings).then(() => {
|
||||
return User.createLocalUsers(users);
|
||||
return SettingsService.init(settings).then(() => {
|
||||
return UsersService.createLocalUsers(users);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
const Action = require('../../models/action');
|
||||
const ActionModel = require('../../models/action');
|
||||
const ActionsService = require('../../services/actions');
|
||||
|
||||
const expect = require('chai').expect;
|
||||
|
||||
describe('models.Action', () => {
|
||||
describe('services.ActionsService', () => {
|
||||
let mockActions = [];
|
||||
|
||||
beforeEach(() => Action.create([
|
||||
beforeEach(() => ActionModel.create([
|
||||
{
|
||||
action_type: 'flag',
|
||||
action_type: 'FLAG',
|
||||
item_id: '123',
|
||||
item_type: 'comment',
|
||||
item_type: 'COMMENTS',
|
||||
user_id: 'flagginguserid'
|
||||
},
|
||||
{
|
||||
action_type: 'flag',
|
||||
action_type: 'FLAG',
|
||||
item_id: '456',
|
||||
item_type: 'comment'
|
||||
item_type: 'COMMENTS'
|
||||
},
|
||||
{
|
||||
action_type: 'flag',
|
||||
action_type: 'FLAG',
|
||||
item_id: '123',
|
||||
item_type: 'comment'
|
||||
item_type: 'COMMENTS'
|
||||
},
|
||||
{
|
||||
action_type: 'like',
|
||||
action_type: 'LIKE',
|
||||
item_id: '123',
|
||||
item_type: 'comment'
|
||||
item_type: 'COMMENTS'
|
||||
}
|
||||
]).then((actions) => {
|
||||
mockActions = actions;
|
||||
@@ -32,16 +34,16 @@ describe('models.Action', () => {
|
||||
|
||||
describe('#findById()', () => {
|
||||
it('should find an action by id', () => {
|
||||
return Action.findById(mockActions[0].id).then((result) => {
|
||||
return ActionsService.findById(mockActions[0].id).then((result) => {
|
||||
expect(result).to.not.be.null;
|
||||
expect(result).to.have.property('action_type', 'flag');
|
||||
expect(result).to.have.property('action_type', 'FLAG');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#findByItemIdArray()', () => {
|
||||
it('should find an array of actions from an array of item_ids', () => {
|
||||
return Action.findByItemIdArray(['123', '456']).then((result) => {
|
||||
return ActionsService.findByItemIdArray(['123', '456']).then((result) => {
|
||||
expect(result).to.have.length(4);
|
||||
});
|
||||
});
|
||||
@@ -49,46 +51,48 @@ describe('models.Action', () => {
|
||||
|
||||
describe('#getActionSummaries()', () => {
|
||||
it('should return properly formatted summaries from an array of item_ids', () => {
|
||||
return Action.getActionSummaries(['123', '789']).then((summaries) => {
|
||||
expect(summaries).to.have.length(2);
|
||||
return ActionsService
|
||||
.getActionSummaries(['123', '789'])
|
||||
.then((summaries) => {
|
||||
expect(summaries).to.have.length(2);
|
||||
|
||||
expect(summaries).to.deep.include({
|
||||
action_type: 'like',
|
||||
count: 1,
|
||||
item_id: '123',
|
||||
item_type: 'comment',
|
||||
current_user: null
|
||||
});
|
||||
expect(summaries).to.deep.include({
|
||||
action_type: 'LIKE',
|
||||
count: 1,
|
||||
item_id: '123',
|
||||
item_type: 'COMMENTS',
|
||||
current_user: null
|
||||
});
|
||||
|
||||
expect(summaries).to.deep.include({
|
||||
action_type: 'flag',
|
||||
count: 2,
|
||||
item_id: '123',
|
||||
item_type: 'comment',
|
||||
current_user: null
|
||||
expect(summaries).to.deep.include({
|
||||
action_type: 'FLAG',
|
||||
count: 2,
|
||||
item_id: '123',
|
||||
item_type: 'COMMENTS',
|
||||
current_user: null
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should include a current user when one is passed', () => {
|
||||
return Action
|
||||
return ActionsService
|
||||
.getActionSummaries(['123'], 'flagginguserid')
|
||||
.then((summaries) => {
|
||||
expect(summaries).to.have.length(2);
|
||||
|
||||
let summary = summaries.find((s) => s.item_id === '123' && s.action_type === 'flag');
|
||||
let summary = summaries.find((s) => s.item_id === '123' && s.action_type === 'FLAG');
|
||||
|
||||
expect(summary).to.not.be.undefined;
|
||||
expect(summary.current_user).to.not.be.null;
|
||||
expect(summary.current_user).to.have.property('item_id', '123');
|
||||
expect(summary.current_user).to.have.property('item_type', 'comment');
|
||||
expect(summary.current_user).to.have.property('item_type', 'COMMENTS');
|
||||
expect(summary.current_user).to.have.property('user_id', 'flagginguserid');
|
||||
expect(summary.current_user).to.have.property('action_type', 'flag');
|
||||
expect(summary.current_user).to.have.property('action_type', 'FLAG');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not include a current user when one is passed for a user that doesn\'t have an action', () => {
|
||||
return Action
|
||||
return ActionsService
|
||||
.getActionSummaries(['123'], 'flagginguserid2')
|
||||
.then((summaries) => {
|
||||
expect(summaries).to.have.length(2);
|
||||
@@ -1,4 +1,5 @@
|
||||
const Asset = require('../../models/asset');
|
||||
const AssetModel = require('../../models/asset');
|
||||
const AssetsService = require('../../services/assets');
|
||||
|
||||
const chai = require('chai');
|
||||
const expect = chai.expect;
|
||||
@@ -6,16 +7,16 @@ const expect = chai.expect;
|
||||
// Use the chai should.
|
||||
chai.should();
|
||||
|
||||
describe('models.Asset', () => {
|
||||
describe('services.AssetsService', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
const defaults = {url:'http://test.com'};
|
||||
return Asset.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
|
||||
return AssetModel.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
|
||||
});
|
||||
|
||||
describe('#findById', ()=> {
|
||||
it('should find an asset by the id', () => {
|
||||
return Asset.findById(1)
|
||||
return AssetsService.findById(1)
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('url')
|
||||
.and.to.equal('http://test.com');
|
||||
@@ -24,17 +25,19 @@ describe('models.Asset', () => {
|
||||
});
|
||||
|
||||
describe('#findByUrl', ()=> {
|
||||
beforeEach(() => Asset.findOrCreateByUrl('http://test.com'));
|
||||
beforeEach(() => AssetsService.findOrCreateByUrl('http://test.com'));
|
||||
|
||||
it('should find an asset by a url', () => {
|
||||
return Asset.findByUrl('http://test.com')
|
||||
return AssetsService
|
||||
.findByUrl('http://test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('url', 'http://test.com');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null when a url does not exist', () => {
|
||||
return Asset.findByUrl('http://new.test.com')
|
||||
return AssetsService
|
||||
.findByUrl('http://new.test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.be.null;
|
||||
});
|
||||
@@ -43,7 +46,8 @@ describe('models.Asset', () => {
|
||||
|
||||
describe('#findOrCreateByUrl', ()=> {
|
||||
it('should find an asset by a url', () => {
|
||||
return Asset.findOrCreateByUrl('http://test.com')
|
||||
return AssetsService
|
||||
.findOrCreateByUrl('http://test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('url')
|
||||
.and.to.equal('http://test.com');
|
||||
@@ -51,7 +55,8 @@ describe('models.Asset', () => {
|
||||
});
|
||||
|
||||
it('should return a new asset when the url does not exist', () => {
|
||||
return Asset.findOrCreateByUrl('http://new.test.com')
|
||||
return AssetsService
|
||||
.findOrCreateByUrl('http://new.test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('id')
|
||||
.and.to.not.equal(1);
|
||||
@@ -61,16 +66,16 @@ describe('models.Asset', () => {
|
||||
|
||||
describe('#overrideSettings', () => {
|
||||
it('should update the settings', () => {
|
||||
return Asset
|
||||
return AssetsService
|
||||
.findOrCreateByUrl('https://override.test.com/asset')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('settings');
|
||||
expect(asset.settings).to.be.null;
|
||||
|
||||
return Asset.overrideSettings(asset.id, {moderation: 'pre'});
|
||||
return AssetsService.overrideSettings(asset.id, {moderation: 'pre'});
|
||||
})
|
||||
.then(() => {
|
||||
return Asset.findOrCreateByUrl('https://override.test.com/asset');
|
||||
return AssetsService.findOrCreateByUrl('https://override.test.com/asset');
|
||||
})
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('settings');
|
||||
@@ -82,7 +87,8 @@ describe('models.Asset', () => {
|
||||
|
||||
describe('#findOrCreateByUrl', ()=> {
|
||||
it('should find an asset by a url', () => {
|
||||
return Asset.findOrCreateByUrl('http://test.com')
|
||||
return AssetsService
|
||||
.findOrCreateByUrl('http://test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('url')
|
||||
.and.to.equal('http://test.com');
|
||||
@@ -90,7 +96,8 @@ describe('models.Asset', () => {
|
||||
});
|
||||
|
||||
it('should return a new asset when the url does not exist', () => {
|
||||
return Asset.findOrCreateByUrl('http://new.test.com')
|
||||
return AssetsService
|
||||
.findOrCreateByUrl('http://new.test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('id')
|
||||
.and.to.not.equal(1);
|
||||
@@ -0,0 +1,259 @@
|
||||
const CommentModel = require('../../models/comment');
|
||||
const ActionModel = require('../../models/action');
|
||||
|
||||
const ActionsService = require('../../services/actions');
|
||||
const UsersService = require('../../services/users');
|
||||
const SettingsService = require('../../services/settings');
|
||||
const CommentsService = require('../../services/comments');
|
||||
|
||||
const settings = {id: '1', moderation: 'pre', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
|
||||
|
||||
const expect = require('chai').expect;
|
||||
|
||||
describe('services.CommentsService', () => {
|
||||
const comments = [{
|
||||
body: 'comment 10',
|
||||
asset_id: '123',
|
||||
status_history: [],
|
||||
parent_id: '',
|
||||
author_id: '123',
|
||||
id: '1'
|
||||
}, {
|
||||
body: 'comment 20',
|
||||
asset_id: '123',
|
||||
status_history: [{
|
||||
type: 'ACCEPTED'
|
||||
}],
|
||||
status: 'ACCEPTED',
|
||||
parent_id: '',
|
||||
author_id: '123',
|
||||
id: '2'
|
||||
}, {
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status_history: [],
|
||||
parent_id: '',
|
||||
author_id: '456',
|
||||
id: '3'
|
||||
}, {
|
||||
body: 'comment 40',
|
||||
asset_id: '123',
|
||||
status_history: [{
|
||||
type: 'REJECTED'
|
||||
}],
|
||||
status: 'REJECTED',
|
||||
parent_id: '',
|
||||
author_id: '456',
|
||||
id: '4'
|
||||
}, {
|
||||
body: 'comment 50',
|
||||
asset_id: '1234',
|
||||
status_history: [{
|
||||
type: 'PREMOD'
|
||||
}],
|
||||
status: 'PREMOD',
|
||||
parent_id: '',
|
||||
author_id: '456',
|
||||
id: '5'
|
||||
}, {
|
||||
body: 'comment 60',
|
||||
asset_id: '1234',
|
||||
status_history: [{
|
||||
type: 'PREMOD'
|
||||
}],
|
||||
status: 'PREMOD',
|
||||
parent_id: '',
|
||||
author_id: '456',
|
||||
id: '6'
|
||||
}];
|
||||
|
||||
const users = [{
|
||||
email: 'stampi@gmail.com',
|
||||
displayName: 'Stampi',
|
||||
password: '1Coral!!'
|
||||
}, {
|
||||
email: 'sockmonster@gmail.com',
|
||||
displayName: 'Sockmonster',
|
||||
password: '2Coral!!'
|
||||
}];
|
||||
|
||||
const actions = [{
|
||||
action_type: 'FLAG',
|
||||
item_id: '3',
|
||||
item_type: 'COMMENTS',
|
||||
user_id: '123'
|
||||
}, {
|
||||
action_type: 'LIKE',
|
||||
item_id: '1',
|
||||
item_type: 'COMMENTS',
|
||||
user_id: '456'
|
||||
}];
|
||||
|
||||
beforeEach(() => {
|
||||
return SettingsService.init(settings).then(() => {
|
||||
return Promise.all([
|
||||
CommentModel.create(comments),
|
||||
UsersService.createLocalUsers(users),
|
||||
ActionModel.create(actions)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#publicCreate()', () => {
|
||||
|
||||
it('creates a new comment', () => {
|
||||
return CommentsService
|
||||
.publicCreate({
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED'
|
||||
}).then((c) => {
|
||||
expect(c).to.not.be.null;
|
||||
expect(c.id).to.not.be.null;
|
||||
expect(c.id).to.be.uuid;
|
||||
expect(c.status).to.be.equal('ACCEPTED');
|
||||
});
|
||||
});
|
||||
|
||||
it('creates many new comments', () => {
|
||||
return CommentsService
|
||||
.publicCreate([{
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED'
|
||||
}, {
|
||||
body: 'This is another comment!'
|
||||
}, {
|
||||
body: 'This is a rejected comment!',
|
||||
status: 'REJECTED'
|
||||
}]).then(([c1, c2, c3]) => {
|
||||
expect(c1).to.not.be.null;
|
||||
expect(c1.id).to.be.uuid;
|
||||
expect(c1.status).to.be.equal('ACCEPTED');
|
||||
|
||||
expect(c2).to.not.be.null;
|
||||
expect(c2.id).to.be.uuid;
|
||||
expect(c2.status).to.be.null;
|
||||
|
||||
expect(c3).to.not.be.null;
|
||||
expect(c3.id).to.be.uuid;
|
||||
expect(c3.status).to.be.equal('REJECTED');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#findById()', () => {
|
||||
|
||||
it('should find a comment by id', () => {
|
||||
return CommentsService
|
||||
.findById('1')
|
||||
.then((result) => {
|
||||
expect(result).to.not.be.null;
|
||||
expect(result).to.have.property('body', 'comment 10');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#findByAssetId()', () => {
|
||||
|
||||
it('should find an array of all comments by asset id', () => {
|
||||
return CommentsService
|
||||
.findByAssetId('123')
|
||||
.then((result) => {
|
||||
expect(result).to.have.length(3);
|
||||
result.sort((a, b) => {
|
||||
if (a.body < b.body) {return -1;}
|
||||
else {return 1;}
|
||||
});
|
||||
expect(result[0]).to.have.property('body', 'comment 10');
|
||||
expect(result[1]).to.have.property('body', 'comment 20');
|
||||
expect(result[2]).to.have.property('body', 'comment 40');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#moderationQueue()', () => {
|
||||
|
||||
it('should find an array of new comments to moderate when pre-moderation', () => {
|
||||
return CommentsService
|
||||
.moderationQueue('PREMOD')
|
||||
.then((result) => {
|
||||
expect(result).to.not.be.null;
|
||||
expect(result).to.have.lengthOf(2);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#removeAction', () => {
|
||||
|
||||
it('should remove an action', () => {
|
||||
return CommentsService
|
||||
.removeAction('3', '123', 'flag')
|
||||
.then(() => {
|
||||
return ActionsService.findByItemIdArray(['123']);
|
||||
})
|
||||
.then((actions) => {
|
||||
expect(actions.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#findByUserId', () => {
|
||||
it('should return all comments if admin', () => {
|
||||
return CommentsService
|
||||
.findByUserId('456', true)
|
||||
.then(comments => {
|
||||
expect(comments).to.have.length(4);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return premod and rejected comments if not admin', () => {
|
||||
return CommentsService
|
||||
.findByUserId('456')
|
||||
.then(comments => {
|
||||
expect(comments).to.have.length(1);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#changeStatus', () => {
|
||||
|
||||
it('should change the status of a comment from no status', () => {
|
||||
let comment_id = comments[0].id;
|
||||
|
||||
return CommentsService.findById(comment_id)
|
||||
.then((c) => {
|
||||
expect(c.status).to.be.null;
|
||||
|
||||
return CommentsService.pushStatus(comment_id, 'REJECTED', '123');
|
||||
})
|
||||
.then(() => CommentsService.findById(comment_id))
|
||||
.then((c) => {
|
||||
expect(c).to.have.property('status');
|
||||
expect(c.status).to.equal('REJECTED');
|
||||
expect(c.status_history).to.have.length(1);
|
||||
expect(c.status_history[0]).to.have.property('type', 'REJECTED');
|
||||
expect(c.status_history[0]).to.have.property('assigned_by', '123');
|
||||
});
|
||||
});
|
||||
|
||||
it('should change the status of a comment from accepted', () => {
|
||||
return CommentsService.pushStatus(comments[1].id, 'REJECTED', '123')
|
||||
.then(() => CommentsService.findById(comments[1].id))
|
||||
.then((c) => {
|
||||
expect(c).to.have.property('status_history');
|
||||
expect(c).to.have.property('status');
|
||||
expect(c.status).to.equal('REJECTED');
|
||||
expect(c.status_history).to.have.length(2);
|
||||
expect(c.status_history[0]).to.have.property('type', 'ACCEPTED');
|
||||
expect(c.status_history[0]).to.have.property('assigned_by', null);
|
||||
|
||||
expect(c.status_history[1]).to.have.property('type', 'REJECTED');
|
||||
expect(c.status_history[1]).to.have.property('assigned_by', '123');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
describe('scraper: services', () => {
|
||||
describe('services.scraper', () => {
|
||||
describe('#create', () => {
|
||||
it('should create a new kue job');
|
||||
});
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
const Setting = require('../../models/setting');
|
||||
const SettingsService = require('../../services/settings');
|
||||
const expect = require('chai').expect;
|
||||
|
||||
describe('models.Setting', () => {
|
||||
describe('services.SettingsService', () => {
|
||||
|
||||
beforeEach(() => Setting.init({moderation: 'pre', wordlist: ['donut']}));
|
||||
beforeEach(() => SettingsService.init({moderation: 'pre', wordlist: ['donut']}));
|
||||
|
||||
describe('#retrieve()', () => {
|
||||
it('should have a moderation field defined', () => {
|
||||
return Setting.retrieve().then(settings => {
|
||||
return SettingsService.retrieve().then(settings => {
|
||||
expect(settings).to.have.property('moderation').and.to.equal('pre');
|
||||
});
|
||||
});
|
||||
|
||||
it('should have two infoBox fields defined', () => {
|
||||
return Setting.retrieve().then(settings => {
|
||||
return SettingsService.retrieve().then(settings => {
|
||||
expect(settings).to.have.property('infoBoxEnable').and.to.equal(false);
|
||||
expect(settings).to.have.property('infoBoxContent').and.to.equal('');
|
||||
});
|
||||
@@ -23,7 +23,7 @@ describe('models.Setting', () => {
|
||||
describe('#update()', () => {
|
||||
it('should update the settings with a passed object', () => {
|
||||
const mockSettings = {moderation: 'post', infoBoxEnable: true, infoBoxContent: 'yeah'};
|
||||
return Setting.update(mockSettings).then(updatedSettings => {
|
||||
return SettingsService.update(mockSettings).then(updatedSettings => {
|
||||
expect(updatedSettings).to.be.an('object');
|
||||
expect(updatedSettings).to.have.property('moderation').and.to.equal('post');
|
||||
expect(updatedSettings).to.have.property('infoBoxEnable', true);
|
||||
@@ -34,7 +34,7 @@ describe('models.Setting', () => {
|
||||
|
||||
describe('#get', () => {
|
||||
it('should return the moderation settings', () => {
|
||||
return Setting.retrieve().then(({moderation}) => {
|
||||
return SettingsService.retrieve().then(({moderation}) => {
|
||||
expect(moderation).not.to.be.null;
|
||||
});
|
||||
});
|
||||
@@ -42,7 +42,7 @@ describe('models.Setting', () => {
|
||||
|
||||
describe('#merge', () => {
|
||||
it('should merge a settings object and its overrides', () => {
|
||||
return Setting
|
||||
return SettingsService
|
||||
.retrieve()
|
||||
.then((settings) => {
|
||||
let ovrSett = {moderation: 'post'};
|
||||
@@ -1,16 +1,16 @@
|
||||
const User = require('../../models/user');
|
||||
const Comment = require('../../models/comment');
|
||||
const Setting = require('../../models/setting');
|
||||
const UsersService = require('../../services/users');
|
||||
const SettingsService = require('../../services/settings');
|
||||
|
||||
const expect = require('chai').expect;
|
||||
|
||||
describe('models.User', () => {
|
||||
describe('services.UsersService', () => {
|
||||
|
||||
let mockUsers;
|
||||
beforeEach(() => {
|
||||
const settings = {id: '1', moderation: 'pre', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
|
||||
|
||||
return Setting.init(settings).then(() => {
|
||||
return User.createLocalUsers([{
|
||||
return SettingsService.init(settings).then(() => {
|
||||
return UsersService.createLocalUsers([{
|
||||
email: 'stampi@gmail.com',
|
||||
displayName: 'Stampi',
|
||||
password: '1Coral!-'
|
||||
@@ -30,11 +30,10 @@ describe('models.User', () => {
|
||||
|
||||
describe('#findById()', () => {
|
||||
it('should find a user by id', () => {
|
||||
return User
|
||||
return UsersService
|
||||
.findById(mockUsers[0].id)
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('displayName')
|
||||
.and.to.equal('stampi');
|
||||
expect(user).to.have.property('displayName', 'stampi');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -42,7 +41,7 @@ describe('models.User', () => {
|
||||
describe('#findByIdArray()', () => {
|
||||
it('should find an array of users from an array of ids', () => {
|
||||
const ids = mockUsers.map((user) => user.id);
|
||||
return User.findByIdArray(ids).then((result) => {
|
||||
return UsersService.findByIdArray(ids).then((result) => {
|
||||
expect(result).to.have.length(3);
|
||||
});
|
||||
});
|
||||
@@ -51,15 +50,14 @@ describe('models.User', () => {
|
||||
describe('#findPublicByIdArray()', () => {
|
||||
it('should find an array of users from an array of ids', () => {
|
||||
const ids = mockUsers.map((user) => user.id);
|
||||
return User.findPublicByIdArray(ids).then((result) => {
|
||||
return UsersService.findPublicByIdArray(ids).then((result) => {
|
||||
expect(result).to.have.length(3);
|
||||
const sorted = result.sort((a, b) => {
|
||||
if(a.displayName < b.displayName) {return -1;}
|
||||
if(a.displayName > b.displayName) {return 1;}
|
||||
return 0;
|
||||
});
|
||||
expect(sorted[0]).to.have.property('displayName')
|
||||
.and.to.equal('marvel');
|
||||
expect(sorted[0]).to.have.property('displayName', 'marvel');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -67,7 +65,7 @@ describe('models.User', () => {
|
||||
describe('#findLocalUser', () => {
|
||||
|
||||
it('should find a user when we give the right credentials', () => {
|
||||
return User
|
||||
return UsersService
|
||||
.findLocalUser(mockUsers[0].profiles[0].id, '1Coral!-')
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('displayName')
|
||||
@@ -76,7 +74,7 @@ describe('models.User', () => {
|
||||
});
|
||||
|
||||
it('should not find the user when we give the wrong credentials', () => {
|
||||
return User
|
||||
return UsersService
|
||||
.findLocalUser(mockUsers[0].profiles[0].id, '1Coral!-<nope>')
|
||||
.then((user) => {
|
||||
expect(user).to.equal(false);
|
||||
@@ -87,9 +85,9 @@ describe('models.User', () => {
|
||||
|
||||
describe('#createLocalUser', () => {
|
||||
it('should not create a user with duplicate display name', () => {
|
||||
return User.createLocalUsers([{
|
||||
return UsersService.createLocalUsers([{
|
||||
email: 'otrostampi@gmail.com',
|
||||
displayName: 'Stampi',
|
||||
displayName: 'StampiTheSecond',
|
||||
password: '1Coralito!'
|
||||
}])
|
||||
.then((user) => {
|
||||
@@ -104,7 +102,7 @@ describe('models.User', () => {
|
||||
describe('#createEmailConfirmToken', () => {
|
||||
|
||||
it('should create a token for a valid user', () => {
|
||||
return User
|
||||
return UsersService
|
||||
.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id)
|
||||
.then((token) => {
|
||||
expect(token).to.not.be.null;
|
||||
@@ -112,15 +110,15 @@ describe('models.User', () => {
|
||||
});
|
||||
|
||||
it('should not create a token for a user already verified', () => {
|
||||
return User
|
||||
return UsersService
|
||||
.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id)
|
||||
.then((token) => {
|
||||
expect(token).to.not.be.null;
|
||||
|
||||
return User.verifyEmailConfirmation(token);
|
||||
return UsersService.verifyEmailConfirmation(token);
|
||||
})
|
||||
.then(() => {
|
||||
return User.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id);
|
||||
return UsersService.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id);
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err).to.have.property('message', 'email address already confirmed');
|
||||
@@ -132,17 +130,17 @@ describe('models.User', () => {
|
||||
describe('#verifyEmailConfirmation', () => {
|
||||
|
||||
it('should correctly validate a valid token', () => {
|
||||
return User
|
||||
return UsersService
|
||||
.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id)
|
||||
.then((token) => {
|
||||
expect(token).to.not.be.null;
|
||||
|
||||
return User.verifyEmailConfirmation(token);
|
||||
return UsersService.verifyEmailConfirmation(token);
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly reject an invalid token', () => {
|
||||
return User
|
||||
return UsersService
|
||||
.verifyEmailConfirmation('cats')
|
||||
.catch((err) => {
|
||||
expect(err).to.not.be.null;
|
||||
@@ -150,15 +148,15 @@ describe('models.User', () => {
|
||||
});
|
||||
|
||||
it('should update the user model when verification is complete', () => {
|
||||
return User
|
||||
return UsersService
|
||||
.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id)
|
||||
.then((token) => {
|
||||
expect(token).to.not.be.null;
|
||||
|
||||
return User.verifyEmailConfirmation(token);
|
||||
return UsersService.verifyEmailConfirmation(token);
|
||||
})
|
||||
.then(() => {
|
||||
return User.findById(mockUsers[0].id);
|
||||
return UsersService.findById(mockUsers[0].id);
|
||||
})
|
||||
.then((user) => {
|
||||
expect(user.profiles[0]).to.have.property('metadata');
|
||||
@@ -171,85 +169,42 @@ describe('models.User', () => {
|
||||
|
||||
describe('#setStatus', () => {
|
||||
it('should set the status to active', () => {
|
||||
return User
|
||||
.setStatus(mockUsers[0].id, 'active')
|
||||
.then(() => {
|
||||
User.findById(mockUsers[0].id)
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status')
|
||||
.and.to.equal('active');
|
||||
});
|
||||
return UsersService
|
||||
.setStatus(mockUsers[0].id, 'ACTIVE')
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status', 'ACTIVE');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ban', () => {
|
||||
|
||||
let mockComment;
|
||||
|
||||
beforeEach(() => {
|
||||
return Comment.create([
|
||||
{
|
||||
body: 'testing the comment for that user if it is rejected.',
|
||||
id: mockUsers[0].id
|
||||
}
|
||||
])
|
||||
.then(([comment]) => {
|
||||
mockComment = comment;
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the status to banned', () => {
|
||||
return User
|
||||
.setStatus(mockUsers[0].id, 'banned', mockComment.id)
|
||||
.then(() => {
|
||||
return User.findById(mockUsers[0].id);
|
||||
})
|
||||
return UsersService
|
||||
.setStatus(mockUsers[0].id, 'BANNED')
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status')
|
||||
.and.to.equal('banned');
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the comment to rejected', () => {
|
||||
return User
|
||||
.setStatus(mockUsers[0].id, 'banned', mockComment.id)
|
||||
.then(() => {
|
||||
return Comment.findById(mockComment.id);
|
||||
})
|
||||
.then((comment) => {
|
||||
expect(comment).to.have.property('status')
|
||||
.and.to.equal('rejected');
|
||||
expect(user).to.have.property('status', 'BANNED');
|
||||
});
|
||||
});
|
||||
|
||||
it('should still disable and ban the user if there is no comment', () => {
|
||||
return User
|
||||
.setStatus(mockUsers[0].id, 'banned', '')
|
||||
.then(() => User.findById(mockUsers[0].id))
|
||||
return UsersService
|
||||
.setStatus(mockUsers[0].id, 'BANNED')
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status', 'banned');
|
||||
expect(user).to.have.property('status', 'BANNED');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#unban', () => {
|
||||
let mockComment;
|
||||
beforeEach(() => {
|
||||
return Promise.all([
|
||||
Comment.create([{body: 'testing the comment for that user if it is rejected.', id: mockUsers[0].id}])
|
||||
])
|
||||
.then((comment) => {
|
||||
mockComment = comment;
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the status to active', () => {
|
||||
return User
|
||||
.setStatus(mockUsers[0].id, 'active', mockComment.id)
|
||||
.then(() => User.findById(mockUsers[0].id))
|
||||
return UsersService
|
||||
.setStatus(mockUsers[0].id, 'ACTIVE')
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status', 'active');
|
||||
expect(user).to.have.property('status', 'ACTIVE');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
const expect = require('chai').expect;
|
||||
const Errors = require('../../errors');
|
||||
const Wordlist = require('../../services/wordlist');
|
||||
const Setting = require('../../models/setting');
|
||||
const SettingsService = require('../../services/settings');
|
||||
|
||||
describe('wordlist: services', () => {
|
||||
describe('services.Wordlist', () => {
|
||||
|
||||
const wordlists = {
|
||||
banned: [
|
||||
@@ -19,7 +19,7 @@ describe('wordlist: services', () => {
|
||||
let wordlist = new Wordlist();
|
||||
const settings = {id: '1', moderation: 'pre', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
|
||||
|
||||
beforeEach(() => Setting.init(settings));
|
||||
beforeEach(() => SettingsService.init(settings));
|
||||
|
||||
describe('#init', () => {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user