mirror of
https://github.com/wassname/talk.git
synced 2026-08-04 13:23:35 +08:00
Merge branch 'user-status-refactor' of github.com:coralproject/talk into mod-improv
This commit is contained in:
@@ -52,3 +52,4 @@ plugins/*
|
||||
!plugins/talk-plugin-slack-notifications
|
||||
|
||||
**/node_modules/*
|
||||
yarn-error.log
|
||||
|
||||
+7
-1
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"exec": "npm-run-all --parallel generate-introspection start:development",
|
||||
"verbose": true,
|
||||
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
|
||||
"ext": "js,json,graphql"
|
||||
"ext": "js,json,graphql",
|
||||
"watch": [
|
||||
".",
|
||||
"bin/cli",
|
||||
"bin/cli-serve"
|
||||
]
|
||||
}
|
||||
|
||||
+9
-100
@@ -106,20 +106,17 @@ async function createUser(options) {
|
||||
}
|
||||
|
||||
const user = await UsersService.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim());
|
||||
console.log(`Created user ${user.id}.`);
|
||||
|
||||
if (answers.roles.length > 0) {
|
||||
return Promise.all(answers.roles.map((role) => {
|
||||
return UsersService
|
||||
.addRoleToUser(user.id, role)
|
||||
.then(() => {
|
||||
console.log(`Added the role ${role} to User ${user.id}.`);
|
||||
});
|
||||
}));
|
||||
for (const role of answers.roles) {
|
||||
await UsersService.addRoleToUser(user.id, role);
|
||||
}
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
await UsersService.sendEmailConfirmation(user, answers.email.trim());
|
||||
|
||||
console.log(`Created User ${user.id}.`);
|
||||
util.shutdown();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
@@ -241,12 +238,12 @@ function listUsers() {
|
||||
});
|
||||
|
||||
users.forEach((user) => {
|
||||
let state = user.disabled ? 'Disabled' : 'Enabled';
|
||||
const profile = user.profiles.find(({provider}) => provider === 'local');
|
||||
let state;
|
||||
if (profile && profile.metadata && profile.metadata.confirmed_at) {
|
||||
state += ', Verified';
|
||||
state = 'Verified';
|
||||
} else {
|
||||
state += ', Unverified';
|
||||
state = 'Unverified';
|
||||
}
|
||||
|
||||
table.push([
|
||||
@@ -336,74 +333,6 @@ function removeRole(userID, role) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ban a user
|
||||
* @param {String} userID id of the user to ban
|
||||
*/
|
||||
function ban(userID) {
|
||||
UsersService
|
||||
.setStatus(userID, 'BANNED')
|
||||
.then(() => {
|
||||
console.log(`Banned the User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unban a user
|
||||
* @param {String} userUD id of the user to remove the role from
|
||||
*/
|
||||
function unban(userID) {
|
||||
UsersService
|
||||
.setStatus(userID, 'ACTIVE')
|
||||
.then(() => {
|
||||
console.log(`Unban the User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a given user.
|
||||
* @param {String} userID the ID of a user to disable
|
||||
*/
|
||||
function disableUser(userID) {
|
||||
UsersService
|
||||
.disableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was disabled.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enabled a given user.
|
||||
* @param {String} userID the ID of a user to enable
|
||||
*/
|
||||
function enableUser(userID) {
|
||||
UsersService
|
||||
.enableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was enabled.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies an email address for a user.
|
||||
*
|
||||
@@ -472,26 +401,6 @@ program
|
||||
.description('removes a role from a given user')
|
||||
.action(removeRole);
|
||||
|
||||
program
|
||||
.command('ban <userID>')
|
||||
.description('ban a given user')
|
||||
.action(ban);
|
||||
|
||||
program
|
||||
.command('uban <userID>')
|
||||
.description('unban a given user')
|
||||
.action(unban);
|
||||
|
||||
program
|
||||
.command('disable <userID>')
|
||||
.description('disable a given user from logging in')
|
||||
.action(disableUser);
|
||||
|
||||
program
|
||||
.command('enable <userID>')
|
||||
.description('enable a given user from logging in')
|
||||
.action(enableUser);
|
||||
|
||||
program
|
||||
.command('verify <userID> <email>')
|
||||
.description('verifies the given user\'s email address')
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
const UserModel = require('../../../models/user');
|
||||
const CommentModel = require('../../../models/comment');
|
||||
const ActionsService = require('../../../services/actions');
|
||||
const {arrayJoinBy} = require('../../../graph/loaders/util');
|
||||
const sc = require('snake-case');
|
||||
const debug = require('debug')('talk:cli:verify');
|
||||
|
||||
const MODELS = [
|
||||
UserModel,
|
||||
CommentModel,
|
||||
];
|
||||
|
||||
async function processBatch(Model, documents) {
|
||||
|
||||
// Get an array of all the document id's.
|
||||
const documentIDs = documents.map(({id}) => id);
|
||||
|
||||
// Store all the operations on this batch in this array that we'll return
|
||||
// later.
|
||||
const operations = [];
|
||||
|
||||
// Get the action summaries for this batch.
|
||||
const totalActionSummaries = await ActionsService
|
||||
.getActionSummaries(documentIDs)
|
||||
.then(arrayJoinBy(documentIDs, 'item_id'));
|
||||
|
||||
// Iterate over the documents.
|
||||
for (let i = 0; i < documents.length; i++) {
|
||||
const document = documents[i];
|
||||
const actionSummaries = totalActionSummaries[i];
|
||||
|
||||
let ops = [];
|
||||
|
||||
for (const actionSummary of actionSummaries) {
|
||||
if (actionSummary.group_id === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// And we generate the group id.
|
||||
const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase());
|
||||
const GROUP_ID = sc(actionSummary.group_id.toLowerCase());
|
||||
|
||||
if (GROUP_ID.length <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// And we add a new batch operation if the action summary is associated
|
||||
// with a group.
|
||||
const ACTION_COUNT_FIELD = `${ACTION_TYPE}_${GROUP_ID}`;
|
||||
|
||||
// Check that the action summaries match the cached counts.
|
||||
if (
|
||||
!document.action_counts ||
|
||||
!(ACTION_COUNT_FIELD in document.action_counts) ||
|
||||
document.action_counts[ACTION_COUNT_FIELD] !== actionSummary.count
|
||||
) {
|
||||
|
||||
// Batch updates for those changes.
|
||||
ops.push({
|
||||
[`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Group all the action summaries together from all the different group
|
||||
// ids.
|
||||
let groupedActionSummaries = actionSummaries.reduce((acc, actionSummary) => {
|
||||
const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase());
|
||||
|
||||
if (!(ACTION_TYPE in acc)) {
|
||||
acc[ACTION_TYPE] = 0;
|
||||
}
|
||||
|
||||
acc[ACTION_TYPE] += actionSummary.count;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
for (const ACTION_COUNT_FIELD of Object.keys(groupedActionSummaries)) {
|
||||
const count = groupedActionSummaries[ACTION_COUNT_FIELD];
|
||||
|
||||
// Check that the action summaries match the cached counts.
|
||||
if (
|
||||
!document.action_counts ||
|
||||
!(ACTION_COUNT_FIELD in document.action_counts) ||
|
||||
document.action_counts[ACTION_COUNT_FIELD] !== count
|
||||
) {
|
||||
|
||||
// Batch updates for those changes.
|
||||
ops.push({
|
||||
[`action_counts.${ACTION_COUNT_FIELD}`]: count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If this comment has action summaries that should be updated, then
|
||||
// perform an update!
|
||||
if (ops.length > 0) {
|
||||
operations.push({
|
||||
updateOne: {
|
||||
filter: {
|
||||
id: document.id
|
||||
},
|
||||
update: {
|
||||
$set: Object.assign({}, ...ops),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
module.exports = async ({fix, batch}) => {
|
||||
for (const Model of MODELS) {
|
||||
const cursor = Model
|
||||
.collection
|
||||
.find({})
|
||||
.project({
|
||||
id: 1,
|
||||
action_counts: 1
|
||||
})
|
||||
.sort({created_at: 1});
|
||||
|
||||
let operations = [];
|
||||
let documents = [];
|
||||
|
||||
// While there are documents to process.
|
||||
while (await cursor.hasNext()) {
|
||||
|
||||
// Load the document.
|
||||
const document = await cursor.next();
|
||||
|
||||
// Push the document into the documents array.
|
||||
documents.push(document);
|
||||
|
||||
// Check to see if the length of the documents array requires us to
|
||||
// process it.
|
||||
if (documents.length > batch) {
|
||||
|
||||
// Process this batch.
|
||||
let batchOperations = await processBatch(Model, documents);
|
||||
|
||||
// Push the batch operations into the model operations.
|
||||
operations.push(...batchOperations);
|
||||
|
||||
// Clear this batch contents.
|
||||
documents = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Check to see if there are any documents left over.
|
||||
if (documents.length > 0) {
|
||||
|
||||
// Process this batch.
|
||||
let batchOperations = await processBatch(Model, documents);
|
||||
|
||||
// Push the batch operations into the model operations.
|
||||
operations.push(...batchOperations);
|
||||
}
|
||||
|
||||
const OPERATIONS_LENGTH = operations.length;
|
||||
|
||||
console.log(`action_counts.js: ${OPERATIONS_LENGTH} ${Model.collection.name} need their action counts fixed.`);
|
||||
|
||||
// If fix was enabled, execute the batch writes.
|
||||
if (OPERATIONS_LENGTH > 0) {
|
||||
if (fix) {
|
||||
debug(`action_counts.js: fixing ${OPERATIONS_LENGTH} ${Model.collection.name}...`);
|
||||
|
||||
while (operations.length) {
|
||||
let result = await Model.collection.bulkWrite(operations.splice(0, batch));
|
||||
|
||||
debug(`action_counts.js: fixed batch of ${result.modifiedCount} ${Model.collection.name}.`);
|
||||
}
|
||||
|
||||
console.log(`action_counts.js: applied all ${OPERATIONS_LENGTH} fixes to ${Model.collection.name}.`);
|
||||
} else {
|
||||
console.warn('Skipping fixing, --fix was not enabled, pass --fix to fix these errors');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+1
-64
@@ -1,7 +1,5 @@
|
||||
const CommentModel = require('../../../models/comment');
|
||||
const ActionsService = require('../../../services/actions');
|
||||
const {arrayJoinBy, singleJoinBy} = require('../../../graph/loaders/util');
|
||||
const sc = require('snake-case');
|
||||
const {singleJoinBy} = require('../../../graph/loaders/util');
|
||||
const debug = require('debug')('talk:cli:verify');
|
||||
|
||||
const getBatch = async (limit, offset) => CommentModel
|
||||
@@ -55,15 +53,9 @@ module.exports = async ({fix, limit, batch}) => {
|
||||
.then(singleJoinBy(commentIDs, '_id'))
|
||||
.then((results) => results.map((result) => result ? result.count : 0));
|
||||
|
||||
// Get their action summaries.
|
||||
let allActionSummaries = await ActionsService
|
||||
.getActionSummaries(commentIDs)
|
||||
.then(arrayJoinBy(commentIDs, 'item_id'));
|
||||
|
||||
// Loop over the comments, with their action summaries.
|
||||
for (let i = 0; i < comments.length; i++) {
|
||||
let comment = comments[i];
|
||||
let actionSummaries = allActionSummaries[i];
|
||||
let replyCount = allReplyCounts[i];
|
||||
|
||||
// And check to see if the action summaries we just computed match what is
|
||||
@@ -77,61 +69,6 @@ module.exports = async ({fix, limit, batch}) => {
|
||||
});
|
||||
}
|
||||
|
||||
// First we process all the group id's.
|
||||
for (let actionSummary of actionSummaries) {
|
||||
if (actionSummary.group_id === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// And we generate the group id.
|
||||
const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase());
|
||||
const GROUP_ID = sc(actionSummary.group_id.toLowerCase());
|
||||
|
||||
if (GROUP_ID.length <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// And we add a new batch operation if the action summary is associated
|
||||
// with a group.
|
||||
const ACTION_COUNT_FIELD = `${ACTION_TYPE}_${GROUP_ID}`;
|
||||
|
||||
// Check that the action summaries match the cached counts.
|
||||
if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== actionSummary.count) {
|
||||
|
||||
// Batch updates for those changes.
|
||||
commentOperations.push({
|
||||
[`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Group all the action summaries together from all the different group
|
||||
// ids.
|
||||
let groupedActionSummaries = actionSummaries.reduce((acc, actionSummary) => {
|
||||
const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase());
|
||||
|
||||
if (!(ACTION_TYPE in acc)) {
|
||||
acc[ACTION_TYPE] = 0;
|
||||
}
|
||||
|
||||
acc[ACTION_TYPE] += actionSummary.count;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
for (const ACTION_COUNT_FIELD of Object.keys(groupedActionSummaries)) {
|
||||
const count = groupedActionSummaries[ACTION_COUNT_FIELD];
|
||||
|
||||
// Check that the action summaries match the cached counts.
|
||||
if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== count) {
|
||||
|
||||
// Batch updates for those changes.
|
||||
commentOperations.push({
|
||||
[`action_counts.${ACTION_COUNT_FIELD}`]: count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If this comment has action summaries that should be updated, then
|
||||
// perform an update!
|
||||
if (commentOperations.length > 0) {
|
||||
@@ -6,7 +6,8 @@
|
||||
//
|
||||
// async ({fix = false, batch = 1000}) => {}
|
||||
//
|
||||
// where their options are derrived.
|
||||
// where their options are derived.
|
||||
module.exports = [
|
||||
require('./comments'),
|
||||
require('./comment_replies'),
|
||||
require('./action_counts'),
|
||||
];
|
||||
|
||||
+5
-2
@@ -7,6 +7,8 @@ machine:
|
||||
environment:
|
||||
PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
|
||||
NODE_ENV: "test"
|
||||
MOCHA_FILE: "${CIRCLE_TEST_REPORTS}/junit/test-results.xml"
|
||||
MOCHA_REPORTER: "mocha-junit-reporter"
|
||||
pre:
|
||||
# TODO: use the following to add in support for MongoDB 3.4.
|
||||
# # Upgrade the database version to 3.4.
|
||||
@@ -47,10 +49,11 @@ database:
|
||||
test:
|
||||
override:
|
||||
# Run the tests using the junit reporter.
|
||||
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test
|
||||
- yarn test
|
||||
# Run the end to end tests
|
||||
- yarn e2e:ci
|
||||
# Check dependancies using nsp.
|
||||
- nsp check
|
||||
- yarn e2e-ci
|
||||
|
||||
deployment:
|
||||
release:
|
||||
|
||||
@@ -14,7 +14,7 @@ export default withQuery(gql`
|
||||
})
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
statuses: [SET, CHANGED]
|
||||
})
|
||||
}
|
||||
`, {
|
||||
|
||||
@@ -22,7 +22,7 @@ const withData = withQuery(gql`
|
||||
query TalkAdmin_Community {
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
statuses: [SET, CHANGED]
|
||||
})
|
||||
...${getDefinitionName(FlaggedAccounts.fragments.root)}
|
||||
...${getDefinitionName(FlaggedUser.fragments.root)}
|
||||
|
||||
@@ -26,4 +26,4 @@ CloseCommentsInfo.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default CloseCommentsInfo;
|
||||
export default CloseCommentsInfo;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
.bio textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
min-height: 100px;
|
||||
margin: 10px 0;
|
||||
border: solid 1px #d8d8d8;
|
||||
}
|
||||
|
||||
.bio h1 {
|
||||
font-size: 16px;
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.bio p {
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './Bio.css';
|
||||
import {Button} from '../../coral-ui';
|
||||
|
||||
export default ({bio, handleSave, handleInput, handleCancel}) => (
|
||||
<div className={styles.bio}>
|
||||
<h1>Bio</h1>
|
||||
<p>Tell the community about yourself</p>
|
||||
<form>
|
||||
<textarea value={bio} onChange={handleInput} />
|
||||
<div className={styles.actions}>
|
||||
<Button cStyle='cancel' type="button" onClick={handleCancel} raised>Cancel</Button>
|
||||
<Button cStyle='success' type="submit" onClick={handleSave}>Save Changes</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import Bio from '../components/Bio';
|
||||
|
||||
export default class BioContainer extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
bio: props.bio
|
||||
};
|
||||
|
||||
this.handleSave = this.handleSave.bind(this);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.handleCancel = this.handleCancel.bind(this);
|
||||
}
|
||||
|
||||
handleInput(e) {
|
||||
this.setState({
|
||||
bio: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
handleSave (e) {
|
||||
e.preventDefault();
|
||||
const {userData, saveBio} = this.props;
|
||||
const {bio} = this.state;
|
||||
saveBio(userData.id, {bio});
|
||||
}
|
||||
|
||||
handleCancel () {
|
||||
this.setState({
|
||||
bio: this.props.bio
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Bio
|
||||
bio={this.state.bio}
|
||||
userData={this.props.userData}
|
||||
handleSave={this.handleSave}
|
||||
handleInput={this.handleInput}
|
||||
handleCancel={this.handleCancel}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {PopupMenu, Button} from 'coral-ui';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import cn from 'classnames';
|
||||
import styles from './styles.css';
|
||||
import * as REASONS from '../helpers/flagReasons';
|
||||
|
||||
import {getErrorMessages} from 'coral-framework/utils';
|
||||
|
||||
@@ -90,10 +91,9 @@ export default class FlagButton extends Component {
|
||||
let action = {
|
||||
item_id,
|
||||
item_type: itemType,
|
||||
reason: null,
|
||||
message
|
||||
};
|
||||
if (reason === 'COMMENT_NOAGREE') {
|
||||
if (reason === REASONS.comment.noagree) {
|
||||
postDontAgree(action)
|
||||
.then(({data}) => {
|
||||
if (itemType === 'COMMENTS') {
|
||||
@@ -122,7 +122,7 @@ export default class FlagButton extends Component {
|
||||
onPopupOptionClick = (sets) => (e) => {
|
||||
|
||||
// If flagging a user, indicate that this is referencing the username rather than the bio
|
||||
if(sets === 'itemType' && e.target.value === 'users') {
|
||||
if (sets === 'itemType' && e.target.value === 'users') {
|
||||
this.setState({field: 'username'});
|
||||
}
|
||||
|
||||
|
||||
@@ -244,4 +244,4 @@ module.exports = {
|
||||
ErrSpecialChars,
|
||||
ErrUsernameTaken,
|
||||
ExtendableError,
|
||||
};
|
||||
};
|
||||
|
||||
+16
-2
@@ -1,12 +1,26 @@
|
||||
const DataLoader = require('dataloader');
|
||||
const TagsService = require('../../services/tags');
|
||||
const plugins = require('../../services/plugins');
|
||||
const debug = require('debug')('talk:graph:loaders:tags');
|
||||
const PLUGIN_TAGS = plugins.get('server', 'tags').reduce((acc, {plugin, tags}) => {
|
||||
debug(`added plugin '${plugin.name}'`);
|
||||
|
||||
acc = acc.concat(tags);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get all the tags for the context for the dataloader.
|
||||
*/
|
||||
const genAll = (context, queries) => {
|
||||
return Promise.all(queries.map(({id, item_type, asset_id}) => {
|
||||
return TagsService.getAll({id, item_type, asset_id});
|
||||
return Promise.all(queries.map(async ({id, item_type, asset_id}) => {
|
||||
let tags = await TagsService.getAll({id, item_type, asset_id});
|
||||
|
||||
// Merge in the global plugin tags as well.
|
||||
tags = tags.concat(PLUGIN_TAGS);
|
||||
|
||||
return tags;
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
+67
-25
@@ -1,7 +1,7 @@
|
||||
const DataLoader = require('dataloader');
|
||||
|
||||
const util = require('./util');
|
||||
const union = require('lodash/union');
|
||||
const sc = require('snake-case');
|
||||
|
||||
const {
|
||||
SEARCH_OTHER_USERS,
|
||||
@@ -10,6 +10,47 @@ const {
|
||||
const UsersService = require('../../services/users');
|
||||
const UserModel = require('../../models/user');
|
||||
|
||||
const mergeState = (query, state) => {
|
||||
const {status} = state;
|
||||
|
||||
if (status) {
|
||||
const {username, banned, suspended} = status;
|
||||
|
||||
if (typeof username !== 'undefined' && username && username.length > 0) {
|
||||
query.merge({
|
||||
'status.username.status': {
|
||||
$in: username
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof banned !== 'undefined' && banned !== null) {
|
||||
query.merge({
|
||||
'status.banned.status': banned
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof suspended !== 'undefined' && suspended !== null) {
|
||||
if (suspended) {
|
||||
query.merge({
|
||||
'status.suspension.until': {
|
||||
$gte: Date.now()
|
||||
}
|
||||
});
|
||||
} else {
|
||||
query.merge({
|
||||
$or: [
|
||||
{'status.suspension.until': null},
|
||||
{'status.suspension.until': {
|
||||
$lt: Date.now()
|
||||
}}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const genUserByIDs = async (context, ids) => {
|
||||
if (!ids || ids.length === 0) {
|
||||
return [];
|
||||
@@ -31,23 +72,24 @@ const genUserByIDs = async (context, ids) => {
|
||||
* @param {Object} context graph context
|
||||
* @param {Object} query query terms to apply to the users query
|
||||
*/
|
||||
const getUsersByQuery = async ({user, loaders: {Actions}}, {ids, limit, cursor, statuses, action_type, sortOrder}) => {
|
||||
const getUsersByQuery = async ({user}, {ids, limit, cursor, state, action_type, sortOrder}) => {
|
||||
let query = UserModel.find();
|
||||
|
||||
if (action_type || statuses) {
|
||||
if (action_type || state) {
|
||||
if (!user || !user.can(SEARCH_OTHER_USERS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (statuses) {
|
||||
query = query.where({
|
||||
status: {
|
||||
$in: statuses
|
||||
if (state) {
|
||||
mergeState(query, state);
|
||||
}
|
||||
|
||||
if (action_type) {
|
||||
query.merge({
|
||||
[`action_counts.${sc(action_type.toLowerCase())}`]: {
|
||||
$gt: 0
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const userIds = await Actions.getByTypes({action_type, item_type: 'USERS'});
|
||||
ids = ids ? union(ids, userIds) : userIds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,25 +157,25 @@ const getUsersByQuery = async ({user, loaders: {Actions}}, {ids, limit, cursor,
|
||||
* @return {Promise} resolves to the counts of the users from the
|
||||
* query
|
||||
*/
|
||||
const getCountByQuery = async ({loaders: {Actions}}, {action_type, statuses}) => {
|
||||
const getCountByQuery = async ({user}, {action_type, state}) => {
|
||||
let query = UserModel.find();
|
||||
|
||||
if (action_type) {
|
||||
const userIds = await Actions.getByTypes({action_type, item_type: 'USERS'});
|
||||
if (action_type || state) {
|
||||
if (!user || !user.can(SEARCH_OTHER_USERS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
query = query.find({
|
||||
id: {
|
||||
$in: userIds
|
||||
}
|
||||
});
|
||||
}
|
||||
if (state) {
|
||||
mergeState(query, state);
|
||||
}
|
||||
|
||||
if (statuses) {
|
||||
query = query.where({
|
||||
status: {
|
||||
$in: statuses
|
||||
}
|
||||
});
|
||||
if (action_type) {
|
||||
query.merge({
|
||||
[`action_counts.${sc(action_type.toLowerCase())}`]: {
|
||||
$gt: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return UserModel
|
||||
|
||||
+60
-36
@@ -1,27 +1,57 @@
|
||||
const ActionsService = require('../../services/actions');
|
||||
const UsersService = require('../../services/users');
|
||||
const errors = require('../../errors');
|
||||
const {CREATE_ACTION, DELETE_ACTION} = require('../../perms/constants');
|
||||
|
||||
/**
|
||||
* getActionItem will return the item that is associated with the given action.
|
||||
* If it does not exist, it will throw an error.
|
||||
*
|
||||
* @param {Object} ctx the graphql context for the request
|
||||
* @param {Object} action the action being performed
|
||||
* @return {Promise} resolves to the referenced item
|
||||
*/
|
||||
const getActionItem = async ({loaders: {Comments, Users}}, {item_id, item_type}) => {
|
||||
if (item_type === 'COMMENTS') {
|
||||
const comment = await Comments.get.load(item_id);
|
||||
if (!comment) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
return comment;
|
||||
} else if (item_type === 'USERS') {
|
||||
const user = await Users.getByID.load(item_id);
|
||||
if (!user) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an action on a item. If the item is a user flag, sets the user's status to
|
||||
* pending.
|
||||
* @param {Object} user the user performing the request
|
||||
* @param {String} item_id id of the item to add the action to
|
||||
* @param {String} item_type type of the item
|
||||
* @param {String} action_type type of the action
|
||||
* @return {Promise} resolves to the action created
|
||||
*
|
||||
* @param {Object} ctx the graphql context for the request
|
||||
* @param {Object} action the action being created
|
||||
* @return {Promise} resolves to the action created
|
||||
*/
|
||||
const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id, item_type, action_type, group_id, metadata = {}}) => {
|
||||
const createAction = async (ctx, {item_id, item_type, action_type, group_id, metadata = {}}) => {
|
||||
const {user = {}, pubsub} = ctx;
|
||||
|
||||
let comment;
|
||||
if (item_type === 'COMMENTS') {
|
||||
comment = await Comments.get.load(item_id);
|
||||
if (!comment) {
|
||||
throw new Error('Comment not found');
|
||||
// Gets the item referenced by the action.
|
||||
const item = await getActionItem(ctx, {item_id, item_type});
|
||||
|
||||
if (action_type === 'FLAG' && item_type === 'USERS') {
|
||||
|
||||
// The item is a user, and this is a flag. Check to see if they are staff,
|
||||
// if they are, don't permit the flag.
|
||||
if (item.isStaff()) {
|
||||
throw errors.ErrNotAuthorized;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the action itself.
|
||||
let action = await ActionsService.create({
|
||||
item_id,
|
||||
item_type,
|
||||
@@ -31,17 +61,11 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id,
|
||||
metadata
|
||||
});
|
||||
|
||||
if (item_type === 'USERS' && action_type === 'FLAG') {
|
||||
if (action_type === 'FLAG' && item_type === 'COMMENTS') {
|
||||
|
||||
// Set the user as pending if it was a user flag and user has no Admin, Staff or Moderation roles
|
||||
let user = await UsersService.findById(item_id);
|
||||
if(!user.isStaff()){
|
||||
await UsersService.setStatus(item_id, 'PENDING');
|
||||
}
|
||||
}
|
||||
|
||||
if (comment) {
|
||||
pubsub.publish('commentFlagged', comment);
|
||||
// The item is a comment, and this is a flag. Push that the comment was
|
||||
// flagged, don't wait for it to finish.
|
||||
pubsub.publish('commentFlagged', item);
|
||||
}
|
||||
|
||||
return action;
|
||||
@@ -49,28 +73,28 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id,
|
||||
|
||||
/**
|
||||
* Deletes an action based on the user id if the user owns that action.
|
||||
*
|
||||
* @param {Object} user the user performing the request
|
||||
* @param {String} id the id of the action to delete
|
||||
* @return {Promise} resolves to the deleted action, or null if not found.
|
||||
*/
|
||||
const deleteAction = ({user}, {id}) => {
|
||||
return ActionsService.delete({id, user_id: user.id});
|
||||
};
|
||||
const deleteAction = ({user}, {id}) => ActionsService.delete({id, user_id: user.id});
|
||||
|
||||
module.exports = (context) => {
|
||||
if (context.user && context.user.can(CREATE_ACTION, DELETE_ACTION)) {
|
||||
return {
|
||||
Action: {
|
||||
create: (action) => createAction(context, action),
|
||||
delete: (action) => deleteAction(context, action)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
module.exports = (ctx) => {
|
||||
const mutators = {
|
||||
Action: {
|
||||
create: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
delete: () => Promise.reject(errors.ErrNotAuthorized)
|
||||
}
|
||||
};
|
||||
|
||||
if (ctx.user && ctx.user.can(CREATE_ACTION)) {
|
||||
mutators.Action.create = (action) => createAction(ctx, action);
|
||||
}
|
||||
|
||||
if (ctx.user && ctx.user.can(DELETE_ACTION)) {
|
||||
mutators.Action.delete = (action) => deleteAction(ctx, action);
|
||||
}
|
||||
|
||||
return mutators;
|
||||
};
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
const errors = require('../../errors');
|
||||
|
||||
const ActionModel = require('../../models/action');
|
||||
const AssetsService = require('../../services/assets');
|
||||
const ActionsService = require('../../services/actions');
|
||||
const TagsService = require('../../services/tags');
|
||||
const CommentsService = require('../../services/comments');
|
||||
const KarmaService = require('../../services/karma');
|
||||
const tlds = require('tlds');
|
||||
const merge = require('lodash/merge');
|
||||
const linkify = require('linkify-it')()
|
||||
.tlds(tlds);
|
||||
const linkify = require('linkify-it')().tlds(require('tlds'));
|
||||
const Wordlist = require('../../services/wordlist');
|
||||
const {
|
||||
CREATE_COMMENT,
|
||||
@@ -17,21 +14,8 @@ const {
|
||||
ADD_COMMENT_TAG,
|
||||
EDIT_COMMENT
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const {
|
||||
DISABLE_AUTOFLAG_SUSPECT_WORDS
|
||||
} = require('../../config');
|
||||
|
||||
const debug = require('debug')('talk:graph:mutators:tags');
|
||||
const plugins = require('../../services/plugins');
|
||||
|
||||
const pluginTags = plugins.get('server', 'tags').reduce((acc, {plugin, tags}) => {
|
||||
debug(`added plugin '${plugin.name}'`);
|
||||
|
||||
acc = acc.concat(tags);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
const debug = require('debug')('talk:graph:mutators:comment');
|
||||
const {DISABLE_AUTOFLAG_SUSPECT_WORDS} = require('../../config');
|
||||
|
||||
const resolveTagsForComment = async ({user, loaders: {Tags}}, {asset_id, tags = []}) => {
|
||||
const item_type = 'COMMENTS';
|
||||
@@ -48,8 +32,6 @@ const resolveTagsForComment = async ({user, loaders: {Tags}}, {asset_id, tags =
|
||||
globalTags = [];
|
||||
}
|
||||
|
||||
globalTags = globalTags.concat(pluginTags);
|
||||
|
||||
// Merge in the tags for the given comment.
|
||||
tags = tags.map((name) => {
|
||||
|
||||
@@ -285,14 +267,14 @@ const moderationPhases = [
|
||||
actions: [{
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'Matched suspect word filter',
|
||||
group_id: 'SUSPECT_WORD',
|
||||
metadata: {}
|
||||
}],
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// This phase checks to see if the comment's length exeeds maximum.
|
||||
// This phase checks to see if the comment's length exceeds maximum.
|
||||
(context, comment, {assetSettings: {charCountEnable, charCount}}) => {
|
||||
|
||||
// Reject if the comment is too long
|
||||
@@ -320,7 +302,7 @@ const moderationPhases = [
|
||||
|
||||
// Add the flag related to Trust to the comment.
|
||||
return {
|
||||
status:'SYSTEM_WITHHELD',
|
||||
status: 'SYSTEM_WITHHELD',
|
||||
actions: [{
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
@@ -362,7 +344,7 @@ const moderationPhases = [
|
||||
}
|
||||
},
|
||||
|
||||
// This phase checks to see if the comment was already perscribed a status.
|
||||
// This phase checks to see if the comment was already prescribed a status.
|
||||
(context, comment) => {
|
||||
|
||||
// If the status was already defined, don't redefine it. It's only defined
|
||||
|
||||
@@ -11,7 +11,7 @@ const modify = async ({user, loaders: {Tags}}, operation, {name, id, item_type,
|
||||
const tags = await Tags.getAll.load({id, item_type, asset_id});
|
||||
|
||||
// Resolve the TagLink that should be used to insert to the user. This will
|
||||
// addtionally return with an ownership property that can be used to determine
|
||||
// additionally return with an ownership property that can be used to determine
|
||||
// that the user who adds this tag must also be the owner of the resource.
|
||||
let {tagLink, ownership} = TagsService.resolveLink(user, tags, {name, item_type});
|
||||
|
||||
|
||||
+58
-30
@@ -1,29 +1,34 @@
|
||||
const errors = require('../../errors');
|
||||
const UsersService = require('../../services/users');
|
||||
const {SET_USER_STATUS, SUSPEND_USER, REJECT_USERNAME} = require('../../perms/constants');
|
||||
const {
|
||||
CHANGE_USERNAME,
|
||||
SET_USERNAME,
|
||||
SET_USER_USERNAME_STATUS,
|
||||
SET_USER_BAN_STATUS,
|
||||
SET_USER_SUSPENSION_STATUS,
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const setUserStatus = async ({pubsub}, {id, status}) => {
|
||||
const result = await UsersService.setStatus(id, status);
|
||||
if (result && result.status === 'BANNED') {
|
||||
pubsub.publish('userBanned', result);
|
||||
const setUserUsernameStatus = async (ctx, id, status) => {
|
||||
const user = await UsersService.setUsernameStatus(id, status, ctx.user.id);
|
||||
if (status === 'REJECTED') {
|
||||
ctx.pubsub.publish('usernameRejected', user);
|
||||
} else if (status === 'APPROVED') {
|
||||
ctx.pubsub.publish('usernameApproved', user);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const suspendUser = async ({pubsub}, {id, message, until}) => {
|
||||
const result = await UsersService.suspendUser(id, message, until);
|
||||
if (result) {
|
||||
pubsub.publish('userSuspended', result);
|
||||
const setUserBanStatus = async (ctx, id, status) => {
|
||||
const user = await UsersService.setBanStatus(id, status, ctx.user.id);
|
||||
if (user.banned) {
|
||||
ctx.pubsub.publish('userBanned', user);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const rejectUsername = async ({pubsub}, {id, message}) => {
|
||||
const result = await UsersService.rejectUsername(id, message);
|
||||
if (result) {
|
||||
pubsub.publish('usernameRejected', result);
|
||||
const setUserSuspensionStatus = async (ctx, id, until) => {
|
||||
const user = await UsersService.setSuspensionStatus(id, until, ctx.user.id);
|
||||
if (user.suspended) {
|
||||
ctx.pubsub.publish('userSuspended', user);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const ignoreUser = ({user}, userToIgnore) => {
|
||||
@@ -34,27 +39,50 @@ const stopIgnoringUser = ({user}, userToStopIgnoring) => {
|
||||
return UsersService.stopIgnoringUsers(user.id, [userToStopIgnoring.id]);
|
||||
};
|
||||
|
||||
module.exports = (context) => {
|
||||
const changeUsername = async (ctx, id, username) => {
|
||||
return UsersService.changeUsername(id, username);
|
||||
};
|
||||
|
||||
const setUsername = async (ctx, id, username) => {
|
||||
return UsersService.setUsername(id, username);
|
||||
};
|
||||
|
||||
module.exports = (ctx) => {
|
||||
let mutators = {
|
||||
User: {
|
||||
setUserStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
suspendUser: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
rejectUsername: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
ignoreUser: (action) => ignoreUser(context, action),
|
||||
stopIgnoringUser: (action) => stopIgnoringUser(context, action),
|
||||
ignoreUser: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
changeUsername: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setUsername: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
stopIgnoringUser: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setUserUsernameStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setUserBanStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setUserSuspensionStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
}
|
||||
};
|
||||
|
||||
if (context.user && context.user.can(SET_USER_STATUS)) {
|
||||
mutators.User.setUserStatus = (action) => setUserStatus(context, action);
|
||||
}
|
||||
if (ctx.user) {
|
||||
mutators.User.ignoreUser = (action) => ignoreUser(ctx, action);
|
||||
mutators.User.stopIgnoringUser = (action) => stopIgnoringUser(ctx, action);
|
||||
|
||||
if (context.user && context.user.can(SUSPEND_USER)) {
|
||||
mutators.User.suspendUser = (action) => suspendUser(context, action);
|
||||
}
|
||||
if (ctx.user.can(CHANGE_USERNAME)) {
|
||||
mutators.User.changeUsername = (id, username) => changeUsername(ctx, id, username);
|
||||
}
|
||||
|
||||
if (context.user && context.user.can(REJECT_USERNAME)) {
|
||||
mutators.User.rejectUsername = (action) => rejectUsername(context, action);
|
||||
if (ctx.user.can(SET_USERNAME)) {
|
||||
mutators.User.setUsername = (id, username) => setUsername(ctx, id, username);
|
||||
}
|
||||
|
||||
if (ctx.user.can(SET_USER_USERNAME_STATUS)) {
|
||||
mutators.User.setUserUsernameStatus = (id, status) => setUserUsernameStatus(ctx, id, status);
|
||||
}
|
||||
|
||||
if (ctx.user.can(SET_USER_BAN_STATUS)) {
|
||||
mutators.User.setUserBanStatus = (id, status) => setUserBanStatus(ctx, id, status);
|
||||
}
|
||||
|
||||
if (ctx.user.can(SET_USER_SUSPENSION_STATUS)) {
|
||||
mutators.User.setUserSuspensionStatus = (id, until) => setUserSuspensionStatus(ctx, id, until);
|
||||
}
|
||||
}
|
||||
|
||||
return mutators;
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
const DontAgreeAction = {
|
||||
|
||||
// Stored in the metadata, extract and return.
|
||||
reason({metadata: {reason}}) {
|
||||
return reason;
|
||||
}
|
||||
};
|
||||
const DontAgreeAction = {};
|
||||
|
||||
module.exports = DontAgreeAction;
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
const DontAgreeActionSummary = {
|
||||
reason({group_id}) {
|
||||
return group_id;
|
||||
}
|
||||
};
|
||||
const DontAgreeActionSummary = {};
|
||||
|
||||
module.exports = DontAgreeActionSummary;
|
||||
|
||||
@@ -13,20 +13,29 @@ const RootMutation = {
|
||||
createFlag: async (_, {flag: {item_id, item_type, reason, message}}, {mutators: {Action}}) => ({
|
||||
flag: Action.create({item_id, item_type, action_type: 'FLAG', group_id: reason, metadata: {message}}),
|
||||
}),
|
||||
createDontAgree: async (_, {dontagree: {item_id, item_type, reason, message}}, {mutators: {Action}}) => ({
|
||||
dontagree: await Action.create({item_id, item_type, action_type: 'DONTAGREE', group_id: reason, metadata: {message}}),
|
||||
createDontAgree: async (_, {dontagree: {item_id, item_type, message}}, {mutators: {Action}}) => ({
|
||||
dontagree: await Action.create({item_id, item_type, action_type: 'DONTAGREE', metadata: {message}}),
|
||||
}),
|
||||
deleteAction: async (_, {id}, {mutators: {Action}}) => {
|
||||
await Action.delete({id});
|
||||
},
|
||||
setUserStatus: async (_, {id, status}, {mutators: {User}}) => {
|
||||
await User.setUserStatus({id, status});
|
||||
approveUsername: async (_, {id}, {mutators: {User}}) => {
|
||||
await User.setUserUsernameStatus(id, 'APPROVED');
|
||||
},
|
||||
suspendUser: async (_, {input: {id, message, until}}, {mutators: {User}}) => {
|
||||
await User.suspendUser({id, message, until});
|
||||
rejectUsername: async (_, {id}, {mutators: {User}}) => {
|
||||
await User.setUserUsernameStatus(id, 'REJECTED');
|
||||
},
|
||||
rejectUsername: async (_, {input: {id, message}}, {mutators: {User}}) => {
|
||||
await User.rejectUsername({id, message});
|
||||
changeUsername: async (_, {id, username}, {mutators: {User}}) => {
|
||||
await User.changeUsername(id, username);
|
||||
},
|
||||
setUsername: async (_, {id, username}, {mutators: {User}}) => {
|
||||
await User.setUsername(id, username);
|
||||
},
|
||||
setUserSuspensionStatus: async (_, {input: {id, until}}, {mutators: {User}}) => {
|
||||
await User.setUserSuspensionStatus(id, until);
|
||||
},
|
||||
setUserBanStatus: async (_, {input: {id, status}}, {mutators: {User}}) => {
|
||||
await User.setUserBanStatus(id, status);
|
||||
},
|
||||
ignoreUser: async (_, {id}, {mutators: {User}}) => {
|
||||
await User.ignoreUser({id});
|
||||
|
||||
@@ -6,7 +6,6 @@ const {
|
||||
SEARCH_OTHERS_COMMENTS,
|
||||
UPDATE_USER_ROLES,
|
||||
SEARCH_COMMENT_METRICS,
|
||||
VIEW_SUSPENSION_INFO,
|
||||
LIST_OWN_TOKENS
|
||||
} = require('../../perms/constants');
|
||||
|
||||
@@ -84,12 +83,12 @@ const User = {
|
||||
}
|
||||
},
|
||||
|
||||
suspension({id, suspension}, _, {user}) {
|
||||
if (user.id !== id && !user.can(VIEW_SUSPENSION_INFO)) {
|
||||
return null;
|
||||
}
|
||||
return suspension;
|
||||
}
|
||||
// suspension({id, suspension}, _, {user}) {
|
||||
// if (user.id !== id && !user.can(VIEW_SUSPENSION_INFO)) {
|
||||
// return null;
|
||||
// }
|
||||
// return suspension;
|
||||
// }
|
||||
};
|
||||
|
||||
// Decorate the User type resolver with a tags field.
|
||||
|
||||
+106
-114
@@ -7,131 +7,123 @@ const {
|
||||
SUBSCRIBE_ALL_USER_SUSPENDED,
|
||||
SUBSCRIBE_ALL_USER_BANNED,
|
||||
SUBSCRIBE_ALL_USERNAME_REJECTED,
|
||||
SUBSCRIBE_ALL_USERNAME_APPROVED,
|
||||
} = require('../perms/constants');
|
||||
|
||||
const merge = require('lodash/merge');
|
||||
const debug = require('debug')('talk:graph:setupFunctions');
|
||||
const plugins = require('../services/plugins');
|
||||
|
||||
const setupFunctions = {
|
||||
commentAdded: (options, args, comment, context) => {
|
||||
|
||||
// Only privileged users can subscribe to all assets.
|
||||
if (!args.asset_id && (!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If user subscribes for statuses other than NONE and/or ACCEPTED statuses, it needs
|
||||
// special privileges.
|
||||
if (
|
||||
(!args.statuses || args.statuses.some((status) => !['NONE', 'ACCEPTED'].includes(status))) &&
|
||||
(!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (args.asset_id && comment.asset_id !== args.asset_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (args.statuses && !args.statuses.includes(comment.status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
commentEdited: (options, args, comment, context) => {
|
||||
if (!args.asset_id && (!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_EDITED))) {
|
||||
return false;
|
||||
}
|
||||
return !args.asset_id || comment.asset_id === args.asset_id;
|
||||
},
|
||||
commentFlagged: (options, args, comment, context) => {
|
||||
if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_FLAGGED)) {
|
||||
return false;
|
||||
}
|
||||
return !args.asset_id || comment.asset_id === args.asset_id;
|
||||
},
|
||||
commentAccepted: (options, args, comment, context) => {
|
||||
if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_ACCEPTED)) {
|
||||
return false;
|
||||
}
|
||||
return !args.asset_id || comment.asset_id === args.asset_id;
|
||||
},
|
||||
commentRejected: (options, args) => (comment, context) => {
|
||||
if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_REJECTED)) {
|
||||
return false;
|
||||
}
|
||||
return !args.asset_id || comment.asset_id === args.asset_id;
|
||||
},
|
||||
userSuspended: (options, args, user, context) => {
|
||||
if (
|
||||
!context.user
|
||||
|| args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_SUSPENDED)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !args.user_id || user.id === args.user_id;
|
||||
},
|
||||
userBanned: (options, args, user, context) => {
|
||||
if (
|
||||
!context.user
|
||||
|| args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_BANNED)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !args.user_id || user.id === args.user_id;
|
||||
},
|
||||
usernameRejected: (options, args, user, context) => {
|
||||
if (
|
||||
!context.user
|
||||
|| args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USERNAME_REJECTED)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !args.user_id || user.id === args.user_id;
|
||||
},
|
||||
usernameApproved: (options, args, user, context) => {
|
||||
if (
|
||||
!context.user
|
||||
|| args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USERNAME_APPROVED)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !args.user_id || user.id === args.user_id;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin support requires that we merge in existing setupFunctions with our new
|
||||
* plugin based ones. This allows plugins to extend existing setupFunctions as well
|
||||
* as provide new ones.
|
||||
* plugin based ones. This allows plugins to extend existing setupFunctions as
|
||||
* well as provide new ones. We'll remap our internal representation of the
|
||||
* setupFunctions into the format needed by Apollo.
|
||||
*/
|
||||
const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plugin, setupFunctions}) => {
|
||||
module.exports = plugins.get('server', 'setupFunctions').reduce((acc, {plugin, setupFunctions}) => {
|
||||
debug(`added plugin '${plugin.name}'`);
|
||||
|
||||
return merge(acc, setupFunctions);
|
||||
}, {
|
||||
commentAdded: (options, args) => ({
|
||||
commentAdded: {
|
||||
filter: (comment, context) => {
|
||||
}, Object.keys(setupFunctions).map((key) => {
|
||||
const filter = setupFunctions[key];
|
||||
|
||||
// Only priviledged users can subscribe to all assets.
|
||||
if (!args.asset_id && (!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If user scubsscribes for statuses other than NONE and/or ACCEPTED statuses, it needs
|
||||
// special priviledges.
|
||||
if (
|
||||
(!args.statuses || args.statuses.some((status) => !['NONE', 'ACCEPTED'].includes(status))) &&
|
||||
(!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (args.asset_id && comment.asset_id !== args.asset_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (args.statuses && !args.statuses.includes(comment.status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return {
|
||||
[key]: (options, args) => ({
|
||||
[key]: {
|
||||
filter: (user, ctx) => filter(options, args, user, ctx)
|
||||
}
|
||||
},
|
||||
}),
|
||||
commentEdited: (options, args) => ({
|
||||
commentEdited: {
|
||||
filter: (comment, context) => {
|
||||
if (!args.asset_id && (!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_EDITED))) {
|
||||
return false;
|
||||
}
|
||||
return !args.asset_id || comment.asset_id === args.asset_id;
|
||||
}
|
||||
},
|
||||
}),
|
||||
commentFlagged: (options, args) => ({
|
||||
commentFlagged: {
|
||||
filter: (comment, context) => {
|
||||
if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_FLAGGED)) {
|
||||
return false;
|
||||
}
|
||||
return !args.asset_id || comment.asset_id === args.asset_id;
|
||||
}
|
||||
},
|
||||
}),
|
||||
commentAccepted: (options, args) => ({
|
||||
commentAccepted: {
|
||||
filter: (comment, context) => {
|
||||
if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_ACCEPTED)) {
|
||||
return false;
|
||||
}
|
||||
return !args.asset_id || comment.asset_id === args.asset_id;
|
||||
}
|
||||
},
|
||||
}),
|
||||
commentRejected: (options, args) => ({
|
||||
commentRejected: {
|
||||
filter: (comment, context) => {
|
||||
if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_REJECTED)) {
|
||||
return false;
|
||||
}
|
||||
return !args.asset_id || comment.asset_id === args.asset_id;
|
||||
}
|
||||
},
|
||||
}),
|
||||
userSuspended: (options, args) => ({
|
||||
userSuspended: {
|
||||
filter: (user, context) => {
|
||||
if (
|
||||
!context.user
|
||||
|| args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_SUSPENDED)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !args.user_id || user.id === args.user_id;
|
||||
}
|
||||
},
|
||||
}),
|
||||
userBanned: (options, args) => ({
|
||||
userBanned: {
|
||||
filter: (user, context) => {
|
||||
if (
|
||||
!context.user
|
||||
|| args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_BANNED)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !args.user_id || user.id === args.user_id;
|
||||
}
|
||||
},
|
||||
}),
|
||||
usernameRejected: (options, args) => ({
|
||||
usernameRejected: {
|
||||
filter: (user, context) => {
|
||||
if (
|
||||
!context.user
|
||||
|| args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USERNAME_REJECTED)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !args.user_id || user.id === args.user_id;
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
module.exports = setupFunctions;
|
||||
})
|
||||
};
|
||||
})
|
||||
.reduce((setupFunction, setupFunctions) => {
|
||||
return merge(setupFunctions, setupFunction);
|
||||
}, {}));
|
||||
|
||||
+195
-68
@@ -64,8 +64,99 @@ type UserProfile {
|
||||
provider: String!
|
||||
}
|
||||
|
||||
type SuspensionInfo {
|
||||
# USER_STATUS_USERNAME is the different states that a username can be in.
|
||||
enum USER_STATUS_USERNAME {
|
||||
# UNSET is used when the username can be changed, and does not necessarily
|
||||
# require moderator action to become active. This can be used when the user
|
||||
# signs up with a social login and has the option of setting their own
|
||||
# username.
|
||||
UNSET
|
||||
|
||||
# SET is used when the username has been set for the first time, but cannot
|
||||
# change without the username being rejected by a moderator and that moderator
|
||||
# agreeing that the username should be allowed to change.
|
||||
SET
|
||||
|
||||
# APPROVED is used when the username was changed, and subsequently approved by
|
||||
# said moderator.
|
||||
APPROVED
|
||||
|
||||
# REJECTED is used when the username was changed, and subsequently rejected by
|
||||
# said moderator.
|
||||
REJECTED
|
||||
|
||||
# CHANGED is used after a user has changed their username after it was
|
||||
# rejected.
|
||||
CHANGED
|
||||
}
|
||||
|
||||
# UserStatusInput describes the queryable components of the UserStatus.
|
||||
input UserStatusInput {
|
||||
# username will restrict the returned users to only those with the given
|
||||
# username status's. If not provided, no filtering will be performed.
|
||||
username: [USER_STATUS_USERNAME!]
|
||||
|
||||
# banned will restrict the returned users to only those that are, or are not
|
||||
# banned. If not provided, no filtering will be performed.
|
||||
banned: Boolean
|
||||
|
||||
# suspended will restrict the returned users to only those that are, or are not
|
||||
# suspended. If not provided, no filtering will be performed.
|
||||
suspended: Boolean
|
||||
}
|
||||
|
||||
type UsernameStatusHistory {
|
||||
status: USER_STATUS_USERNAME!
|
||||
assigned_by: User
|
||||
created_at: Date!
|
||||
}
|
||||
|
||||
type UsernameStatus {
|
||||
status: USER_STATUS_USERNAME!
|
||||
history: [UsernameStatusHistory!]
|
||||
}
|
||||
|
||||
type BannedStatusHistory {
|
||||
status: Boolean!
|
||||
assigned_by: User
|
||||
created_at: Date!
|
||||
}
|
||||
|
||||
type BannedStatus {
|
||||
status: Boolean!
|
||||
history: [BannedStatusHistory!]
|
||||
}
|
||||
|
||||
type SuspensionStatusHistory {
|
||||
until: Date
|
||||
assigned_by: User
|
||||
created_at: Date!
|
||||
}
|
||||
|
||||
type SuspensionStatus {
|
||||
until: Date
|
||||
history: [SuspensionStatusHistory!]
|
||||
}
|
||||
|
||||
type UserStatus {
|
||||
# username is the status of the username.
|
||||
username: UsernameStatus!
|
||||
|
||||
# banned is the bool that determines if the user is banned or not.
|
||||
banned: BannedStatus!
|
||||
|
||||
# suspension is the date that the user is suspended until.
|
||||
suspension: SuspensionStatus!
|
||||
}
|
||||
|
||||
input UserStateInput {
|
||||
status: UserStatusInput
|
||||
}
|
||||
|
||||
# UserState describes the different permission based details for a user.
|
||||
type UserState {
|
||||
# status describes the statuses of different aspects of the user's details.
|
||||
status: UserStatus
|
||||
}
|
||||
|
||||
# Any person who can author comments, create actions, and view comments on a
|
||||
@@ -96,9 +187,6 @@ type User {
|
||||
# the tags on the user
|
||||
tags: [TagLink!]
|
||||
|
||||
# determines whether the user can edit their username
|
||||
canEditName: Boolean
|
||||
|
||||
# ignored users.
|
||||
ignoredUsers: [User!]
|
||||
|
||||
@@ -114,11 +202,7 @@ type User {
|
||||
reliable: Reliability
|
||||
|
||||
# returns user status
|
||||
status: USER_STATUS
|
||||
|
||||
# returns suspension info. Only available to Admins and Moderators
|
||||
# or on own logged in User.
|
||||
suspension: SuspensionInfo
|
||||
state: UserState
|
||||
}
|
||||
|
||||
# UserConnection represents a paginable subset of a user list.
|
||||
@@ -143,8 +227,7 @@ input UsersQuery {
|
||||
# Users returned will only be ones which have at least one action of this.
|
||||
action_type: ACTION_TYPE
|
||||
|
||||
# Current status of a user..
|
||||
statuses: [USER_STATUS!]
|
||||
state: UserStateInput
|
||||
|
||||
# Limit the number of results to be returned.
|
||||
limit: Int = 10
|
||||
@@ -261,7 +344,7 @@ enum ACTION_TYPE {
|
||||
# CommentsQuery allows the ability to query comments by a specific methods.
|
||||
input CommentsQuery {
|
||||
|
||||
# Author of the commente
|
||||
# Author of the comments.
|
||||
author_id: ID
|
||||
|
||||
# Current status of a comment.
|
||||
@@ -351,8 +434,8 @@ input UserCountQuery {
|
||||
# type.
|
||||
action_type: ACTION_TYPE
|
||||
|
||||
# Current status of a user.
|
||||
statuses: [USER_STATUS]
|
||||
# state queries for a specific subset of users with the given state query.
|
||||
state: UserStateInput
|
||||
}
|
||||
|
||||
type EditInfo {
|
||||
@@ -518,6 +601,36 @@ type FlagAssetActionSummary implements AssetActionSummary {
|
||||
actionableItemCount: Int
|
||||
}
|
||||
|
||||
enum FLAG_REASON {
|
||||
|
||||
# The current user thinks that the flagged username is offensive.
|
||||
USERNAME_OFFENSIVE
|
||||
|
||||
# The current user does not like the flagged username.
|
||||
USERNAME_NOLIKE
|
||||
|
||||
# The current user thinks that the flagged username is being used to
|
||||
# impersonate another user.
|
||||
USERNAME_IMPERSONATING
|
||||
|
||||
# The current user thinks that the flagged username is spam.
|
||||
USERNAME_SPAM
|
||||
|
||||
# The current user thinks that the flagged username is wrong for another
|
||||
# reason.
|
||||
USERNAME_OTHER
|
||||
|
||||
# The current user thinks that the flagged comment is offensive.
|
||||
COMMENT_OFFENSIVE
|
||||
|
||||
# The current user thinks that the flagged comment is spam.
|
||||
COMMENT_SPAM
|
||||
|
||||
# The current user thinks that the flagged comment is wrong for another
|
||||
# reason.
|
||||
COMMENT_OTHER
|
||||
}
|
||||
|
||||
# A FLAG action that contains flag metadata.
|
||||
type FlagAction implements Action {
|
||||
|
||||
@@ -546,9 +659,6 @@ type DontAgreeAction implements Action {
|
||||
# The ID of the DontAgree Action.
|
||||
id: ID!
|
||||
|
||||
# The reason for which the DontAgree Action was created.
|
||||
reason: String
|
||||
|
||||
# An optional message sent with the flagging action by the user.
|
||||
message: String
|
||||
|
||||
@@ -581,9 +691,6 @@ type DontAgreeActionSummary implements ActionSummary {
|
||||
# The total count of flags with this reason.
|
||||
count: Int!
|
||||
|
||||
# The reason for which the Flag Action was created.
|
||||
reason: String
|
||||
|
||||
# The don't agree action by the current user against the parent entity with this reason.
|
||||
current_user: DontAgreeAction
|
||||
}
|
||||
@@ -810,14 +917,6 @@ enum SORT_COMMENTS_BY {
|
||||
REPLIES
|
||||
}
|
||||
|
||||
# All queries that can be executed.
|
||||
enum USER_STATUS {
|
||||
ACTIVE
|
||||
BANNED
|
||||
PENDING
|
||||
APPROVED
|
||||
}
|
||||
|
||||
# Metrics for the assets.
|
||||
enum ASSET_METRICS_SORT {
|
||||
|
||||
@@ -948,7 +1047,7 @@ input CreateFlagInput {
|
||||
item_type: ACTION_ITEM_TYPE!
|
||||
|
||||
# The reason for flagging the item.
|
||||
reason: String!
|
||||
reason: FLAG_REASON
|
||||
|
||||
# An optional message sent with the flagging action by the user.
|
||||
message: String
|
||||
@@ -987,34 +1086,18 @@ input CreateDontAgreeInput {
|
||||
# The type of the item for which we are to create the don't agree.
|
||||
item_type: ACTION_ITEM_TYPE!
|
||||
|
||||
# The reason for not agreeing with the item.
|
||||
reason: String
|
||||
|
||||
# An optional message sent with the don't agree action by the user.
|
||||
message: String
|
||||
}
|
||||
|
||||
# Input for suspendUser mutation.
|
||||
input SuspendUserInput {
|
||||
input SetUserSuspensionStatusInput {
|
||||
|
||||
# id of target user.
|
||||
id: ID!
|
||||
|
||||
# message to be sent to the user.
|
||||
message: String!
|
||||
|
||||
# target user will be suspended until this date.
|
||||
until: Date!
|
||||
}
|
||||
|
||||
# Input for rejectUsername mutation.
|
||||
input RejectUsernameInput {
|
||||
|
||||
# id of target user.
|
||||
id: ID!
|
||||
|
||||
# message to be sent to the user.
|
||||
message: String!
|
||||
until: Date
|
||||
}
|
||||
|
||||
# Configurable settings that can be overridden for the Asset. You must specify
|
||||
@@ -1027,7 +1110,7 @@ input AssetSettingsInput {
|
||||
# moderation is the moderation mode for the asset.
|
||||
moderation: MODERATION_MODE
|
||||
|
||||
# questionBoxEnable will enable the Question Boxs' content to be visable above
|
||||
# questionBoxEnable will enable the Question Boxs' content to be visible above
|
||||
# the comment box.
|
||||
questionBoxEnable: Boolean
|
||||
|
||||
@@ -1075,17 +1158,9 @@ type DeleteActionResponse implements Response {
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# SetUserStatusResponse is the response returned with possibly some errors
|
||||
# relating to the delete action attempt.
|
||||
type SetUserStatusResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# SuspendUserResponse is the response returned with possibly some errors
|
||||
# relating to the suspend action attempt.
|
||||
type SuspendUserResponse implements Response {
|
||||
# SetUserSuspensionStatusResponse is the response returned with possibly some
|
||||
# errors relating to the suspend action attempt.
|
||||
type SetUserSuspensionStatusResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
@@ -1218,7 +1293,7 @@ input UpdateSettingsInput {
|
||||
# comment is posted that it can still be edited by the author.
|
||||
editCommentWindowLength: Int
|
||||
|
||||
# wordlist allows chaninging the available wordlists.
|
||||
# wordlist allows changing the available wordlists.
|
||||
wordlist: UpdateWordlistInput
|
||||
|
||||
# domains allows changing the available lists of domains.
|
||||
@@ -1281,6 +1356,41 @@ type RevokeTokenResponse implements Response {
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# SetUserBanStatusInput contains the input to change the ban status of a given
|
||||
# user.
|
||||
input SetUserBanStatusInput {
|
||||
|
||||
# id is the user to set the ban status on.
|
||||
id: ID!
|
||||
|
||||
# status is the ban status to set on the target user.
|
||||
status: Boolean!
|
||||
}
|
||||
|
||||
type SetUserBanStatusResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type SetUsernameStatusResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type ChangeUsernameResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type SetUsernameResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# All mutations for the application are defined on this object.
|
||||
type RootMutation {
|
||||
|
||||
@@ -1299,17 +1409,30 @@ type RootMutation {
|
||||
# Edit a comment
|
||||
editComment(id: ID!, asset_id: ID!, edit: EditCommentInput): EditCommentResponse!
|
||||
|
||||
# Sets User status. Requires the `ADMIN` role.
|
||||
# Sets the suspension status on a given user. Requires the `MODERATOR` role.
|
||||
# Mutation is restricted.
|
||||
setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse
|
||||
setUserSuspensionStatus(input: SetUserSuspensionStatusInput!): SetUserSuspensionStatusResponse
|
||||
|
||||
# Suspends a user. Requires the `ADMIN` role.
|
||||
# Sets the ban status on a given user. Requires the `MODERATOR` role.
|
||||
# Mutation is restricted.
|
||||
suspendUser(input: SuspendUserInput!): SuspendUserResponse
|
||||
setUserBanStatus(input: SetUserBanStatusInput!): SetUserBanStatusResponse
|
||||
|
||||
# Reject a username. Requires the `ADMIN` role.
|
||||
# Mutation is restricted.
|
||||
rejectUsername(input: RejectUsernameInput!): RejectUsernameResponse
|
||||
# Sets the username status on a given user to `APPROVED`. Requires the
|
||||
# `MODERATOR` role. Mutation is restricted.
|
||||
approveUsername(id: ID!): SetUsernameStatusResponse
|
||||
|
||||
# Sets the username status on a given user to `REJECTED`. Requires the
|
||||
# `MODERATOR` role. Mutation is restricted.
|
||||
rejectUsername(id: ID!): SetUsernameStatusResponse
|
||||
|
||||
# Changes the username to the desired username. Mutation is restricted to
|
||||
# those users with permission do to so.
|
||||
changeUsername(id: ID!, username: String!): ChangeUsernameResponse
|
||||
|
||||
# Sets the username to the desired username if the user has not had a chance
|
||||
# to set their username. Mutation is restricted to those users with permission
|
||||
# do to so that have not done so before.
|
||||
setUsername(id: ID!, username: String!): SetUsernameResponse
|
||||
|
||||
# Sets Comment status. Requires the `ADMIN` role.
|
||||
# Mutation is restricted.
|
||||
@@ -1327,7 +1450,7 @@ type RootMutation {
|
||||
|
||||
# Updates the status of an asset allowing you to close/reopen an asset for
|
||||
# commenting.
|
||||
# Mutation is restricted.
|
||||
# Mutation is restricted.
|
||||
updateAssetStatus(id: ID!, input: UpdateAssetStatusInput!): UpdateAssetStatusResponse
|
||||
|
||||
# updateSettings will update the global settings.
|
||||
@@ -1390,6 +1513,10 @@ type Subscription {
|
||||
# `user_id` must match id of current user except for
|
||||
# users with the `ADMIN` or `MODERATOR` role.
|
||||
usernameRejected(user_id: ID): User
|
||||
|
||||
# Gen an update whenever a username has been approved. `user_id` must match id
|
||||
# of current user except for users with the `ADMIN` or `MODERATOR` role.
|
||||
usernameApproved(user_id: ID): User
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
||||
@@ -254,6 +254,24 @@ en:
|
||||
loading_results: "Loading Results"
|
||||
marketing: "This looks like an ad/marketing"
|
||||
moderate_this_stream: "Moderate this stream"
|
||||
flags:
|
||||
reasons:
|
||||
user:
|
||||
username_offensive: "Offensive"
|
||||
username_nolike: "Dislike"
|
||||
username_impersonating: "Impersonation"
|
||||
username_spam: "Spam"
|
||||
username_other: "Other"
|
||||
comment:
|
||||
comment_offensive: "Offensive"
|
||||
comment_spam: "Spam"
|
||||
comment_noagree: "Disagree"
|
||||
comment_other: "Other"
|
||||
suspect_word: "Suspect Word"
|
||||
banned_word: "Banned Word"
|
||||
body_count: "Body exceeds max length"
|
||||
trust: "Trust"
|
||||
links: "Link"
|
||||
modqueue:
|
||||
account: "account flags"
|
||||
actions: Actions
|
||||
|
||||
@@ -249,6 +249,19 @@ es:
|
||||
loading_results: "Cargando Resultados"
|
||||
marketing: "Esto parece una propaganda"
|
||||
moderate_this_stream: "Moderar este hilo de comentarios"
|
||||
flags:
|
||||
reasons:
|
||||
user:
|
||||
username_offensive: "Es ofensivo"
|
||||
username_nolike: "No le gusta"
|
||||
username_impersonating: "Está suplantando identidad"
|
||||
username_spam: "Contiene spam"
|
||||
username_other: "Otra razón"
|
||||
comment:
|
||||
comment_offensive: "Es ofensivo"
|
||||
comment_spam: "Contiene spam"
|
||||
comment_noagree: "No está de acuerdo"
|
||||
comment_other: "Otra razón"
|
||||
modqueue:
|
||||
account: "reportes de cuentas"
|
||||
actions: Acciones
|
||||
|
||||
@@ -17,7 +17,7 @@ module.exports = {
|
||||
}}
|
||||
]);
|
||||
|
||||
// If no comments were found, nothing needes to be done!
|
||||
// If no comments were found, nothing needs to be done!
|
||||
if (comments.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
const ActionModel = require('../models/action');
|
||||
|
||||
const mapping = {
|
||||
COMMENTS: {
|
||||
'Comment contains toxic language': 'TOXIC_COMMENT',
|
||||
'Matched suspect word filter': 'SUSPECT_WORD',
|
||||
'other': 'COMMENT_OTHER',
|
||||
'Other': 'COMMENT_OTHER',
|
||||
'This looks like an ad/marketing': 'COMMENT_SPAM',
|
||||
'This comment is offensive': 'COMMENT_OFFENSIVE',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
async up() {
|
||||
|
||||
// Setup the batch operation.
|
||||
const batch = ActionModel.collection.initializeUnorderedBulkOp();
|
||||
|
||||
for (const item_type in mapping) {
|
||||
const mappings = mapping[item_type];
|
||||
|
||||
for (const OLD_GROUP_ID in mappings) {
|
||||
const NEW_GROUP_ID = mappings[OLD_GROUP_ID];
|
||||
|
||||
// OLD
|
||||
// {
|
||||
// group_id: <OLD_GROUP_ID>
|
||||
// }
|
||||
// NEW
|
||||
// {
|
||||
// group_id: <NEW_GROUP_ID>
|
||||
// }
|
||||
|
||||
batch.find({
|
||||
group_id: OLD_GROUP_ID,
|
||||
item_type,
|
||||
}).update({
|
||||
$set: {
|
||||
group_id: NEW_GROUP_ID,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the batch update operation.
|
||||
await batch.execute();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
const ActionModel = require('../models/action');
|
||||
|
||||
module.exports = {
|
||||
async up() {
|
||||
|
||||
// This will update all the old flags that are 'COMMENT_NOAGREE' to change
|
||||
// them to DONTAGREE actions instead.
|
||||
return ActionModel.update({
|
||||
action_type: 'FLAG',
|
||||
group_id: 'COMMENT_NOAGREE',
|
||||
}, {
|
||||
$set: {
|
||||
action_type: 'DONTAGREE',
|
||||
group_id: null,
|
||||
},
|
||||
}, {
|
||||
multi: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
const UserModel = require('../models/user');
|
||||
const merge = require('lodash/merge');
|
||||
|
||||
const getUserBatch = async () => {
|
||||
let query = {
|
||||
status: {
|
||||
$in: [
|
||||
'ACTIVE',
|
||||
'BANNED',
|
||||
'PENDING',
|
||||
'APPROVED'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Find all the users that need migrating.
|
||||
return UserModel.collection.find(query);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
async up() {
|
||||
|
||||
const created_at = Date.now();
|
||||
|
||||
// Create a new batch operation.
|
||||
let bulk = UserModel.collection.initializeUnorderedBulkOp();
|
||||
|
||||
// Get the first batch of users.
|
||||
let cursor = await getUserBatch();
|
||||
|
||||
while (await cursor.hasNext()) {
|
||||
const user = await cursor.next();
|
||||
|
||||
const {id, status, canEditName, suspension, disabled} = user;
|
||||
|
||||
let update = {
|
||||
$unset: {
|
||||
canEditName: '',
|
||||
suspension: '',
|
||||
disabled: '',
|
||||
},
|
||||
$set: {
|
||||
status: {
|
||||
|
||||
// The username status is specific to each case.
|
||||
username: {
|
||||
history: []
|
||||
},
|
||||
|
||||
// The user is not banned by default.
|
||||
banned: {
|
||||
status: false,
|
||||
history: []
|
||||
},
|
||||
|
||||
// The user is not suspended by default.
|
||||
suspension: {
|
||||
until: null,
|
||||
history: []
|
||||
},
|
||||
},
|
||||
updated_at: created_at
|
||||
},
|
||||
};
|
||||
|
||||
if (disabled) {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
banned: {
|
||||
status: true,
|
||||
history: [{
|
||||
status: true,
|
||||
created_at
|
||||
}]
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If the user has an "until" property of their suspension, then we need
|
||||
// to reflect that in the new status object.
|
||||
if (suspension && suspension.until !== null) {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
suspension: {
|
||||
until: suspension.until,
|
||||
history: [{
|
||||
until: suspension.until,
|
||||
created_at,
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case 'ACTIVE':
|
||||
if (canEditName) {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
username: {
|
||||
status: 'UNSET',
|
||||
history: [{
|
||||
status: 'UNSET',
|
||||
created_at
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
username: {
|
||||
status: 'SET',
|
||||
history: [{
|
||||
status: 'SET',
|
||||
created_at
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'BANNED':
|
||||
if (canEditName) {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
username: {
|
||||
status: 'REJECTED',
|
||||
history: [{
|
||||
status: 'REJECTED',
|
||||
created_at
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
banned: {
|
||||
status: true,
|
||||
history: [{
|
||||
status: true,
|
||||
created_at
|
||||
}]
|
||||
},
|
||||
username: {
|
||||
status: 'SET',
|
||||
history: [{
|
||||
status: 'SET',
|
||||
created_at
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'PENDING':
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
username: {
|
||||
status: 'CHANGED',
|
||||
history: [{
|
||||
status: 'CHANGED',
|
||||
created_at
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'APPROVED':
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
username: {
|
||||
status: 'APPROVED',
|
||||
history: [{
|
||||
status: 'APPROVED',
|
||||
created_at
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`${status} is an invalid status`);
|
||||
}
|
||||
|
||||
bulk.find({id}).updateOne(update);
|
||||
}
|
||||
|
||||
// Execute the bulk update operation.
|
||||
await bulk.execute();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = [
|
||||
'ACTIVE',
|
||||
'BANNED',
|
||||
'PENDING',
|
||||
'APPROVED' // Indicates that the users' username has been approved
|
||||
];
|
||||
@@ -0,0 +1,25 @@
|
||||
module.exports = [
|
||||
|
||||
// UNSET is used when the username can be changed, and does not necessarily
|
||||
// require moderator action to become active. This can be used when the user
|
||||
// signs up with a social login and has the option of setting their own
|
||||
// username.
|
||||
'UNSET',
|
||||
|
||||
// SET is used when the username has been set for the first time, but cannot
|
||||
// change without the username being rejected by a moderator and that moderator
|
||||
// agreeing that the username should be allowed to change.
|
||||
'SET',
|
||||
|
||||
// APPROVED is used when the username was changed, and subsequently approved by
|
||||
// said moderator.
|
||||
'APPROVED',
|
||||
|
||||
// REJECTED is used when the username was changed, and subsequently rejected by
|
||||
// said moderator.
|
||||
'REJECTED',
|
||||
|
||||
// CHANGED is used after a user has changed their username after it was
|
||||
// rejected.
|
||||
'CHANGED',
|
||||
];
|
||||
+112
-33
@@ -10,8 +10,9 @@ const can = require('../perms');
|
||||
// USER_ROLES is the array of roles that is permissible as a user role.
|
||||
const USER_ROLES = require('./enum/user_roles');
|
||||
|
||||
// USER_STATUS is the list of statuses that are permitted for the user status.
|
||||
const USER_STATUS = require('./enum/user_status');
|
||||
// USER_STATUS_USERNAME is the list of statuses that are supported by storing
|
||||
// the username state.
|
||||
const USER_STATUS_USERNAME = require('./enum/user_status_username');
|
||||
|
||||
// ProfileSchema is the mongoose schema defined as the representation of a
|
||||
// User's profile stored in MongoDB.
|
||||
@@ -73,10 +74,6 @@ const UserSchema = new Schema({
|
||||
unique: true
|
||||
},
|
||||
|
||||
// This is true when the user account is disabled, no action should be
|
||||
// acknowledged when they are disabled. Logins are also prevented.
|
||||
disabled: Boolean,
|
||||
|
||||
// This provides a source of identity proof for users who login using the
|
||||
// local provider. A local provider will be assumed for users who do not
|
||||
// have any social profiles.
|
||||
@@ -97,41 +94,91 @@ const UserSchema = new Schema({
|
||||
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 stores the user status information regarding permissions,
|
||||
// capabilities and moderation state.
|
||||
status: {
|
||||
type: String,
|
||||
enum: USER_STATUS,
|
||||
default: 'ACTIVE'
|
||||
},
|
||||
|
||||
// Determines whether the user can edit their username.
|
||||
canEditName: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Username stores the current user status for the username as well as the
|
||||
// history of changes.
|
||||
username: {
|
||||
|
||||
// User's suspension details.
|
||||
suspension: {
|
||||
until: {
|
||||
type: Date,
|
||||
default: null,
|
||||
// Status stores the current username status.
|
||||
status: {
|
||||
type: String,
|
||||
enum: USER_STATUS_USERNAME,
|
||||
},
|
||||
|
||||
// History stores the history of username status changes.
|
||||
history: [{
|
||||
|
||||
// Status stores the historical username status.
|
||||
status: {
|
||||
type: String,
|
||||
enum: USER_STATUS_USERNAME,
|
||||
},
|
||||
|
||||
// assigned_by stores the user id of the user who assigned this status.
|
||||
assigned_by: {type: String, default: null},
|
||||
|
||||
// created_at stores the date when this status was assigned.
|
||||
created_at: {type: Date, default: Date.now}
|
||||
}],
|
||||
},
|
||||
},
|
||||
|
||||
// User's settings
|
||||
settings: {
|
||||
bio: {
|
||||
type: String,
|
||||
default: ''
|
||||
// Banned stores the current user banned status as well as the history of
|
||||
// changes.
|
||||
banned: {
|
||||
|
||||
// Status stores the current user banned status.
|
||||
status: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
history: [{
|
||||
|
||||
// Status stores the historical banned status.
|
||||
status: Boolean,
|
||||
|
||||
// assigned_by stores the user id of the user who assigned this status.
|
||||
assigned_by: {type: String, default: null},
|
||||
|
||||
// created_at stores the date when this status was assigned.
|
||||
created_at: {type: Date, default: Date.now}
|
||||
}],
|
||||
},
|
||||
|
||||
// Suspension stores the current user suspension status as well as the
|
||||
// history of changes.
|
||||
suspension: {
|
||||
|
||||
// until is the date that the user is suspended until.
|
||||
until: {
|
||||
type: Date,
|
||||
default: null,
|
||||
},
|
||||
history: [{
|
||||
|
||||
// until is the date that the user is suspended until.
|
||||
until: Date,
|
||||
|
||||
// assigned_by stores the user id of the user who assigned this status.
|
||||
assigned_by: {type: String, default: null},
|
||||
|
||||
// created_at stores the date when this status was assigned.
|
||||
created_at: {type: Date, default: Date.now}
|
||||
}]
|
||||
}
|
||||
},
|
||||
|
||||
ignoresUsers: [{
|
||||
// IgnoresUsers is an array of user id's that the current user is ignoring.
|
||||
ignoresUsers: [String],
|
||||
|
||||
// user id of another user
|
||||
type: String,
|
||||
}],
|
||||
// Counts to store related to actions taken on the given user.
|
||||
action_counts: {
|
||||
default: {},
|
||||
type: Object,
|
||||
},
|
||||
|
||||
// Tags are added by the self or by administrators.
|
||||
tags: [TagLinkSchema],
|
||||
@@ -158,7 +205,7 @@ const UserSchema = new Schema({
|
||||
}
|
||||
});
|
||||
|
||||
// Add the indixies on the user profile data.
|
||||
// Add the index on the user profile data.
|
||||
UserSchema.index({
|
||||
'profiles.id': 1,
|
||||
'profiles.provider': 1
|
||||
@@ -201,6 +248,38 @@ UserSchema.method('can', function(...actions) {
|
||||
return can(this, ...actions);
|
||||
});
|
||||
|
||||
/**
|
||||
* banned returns true when the user is currently banned, and sets the banned
|
||||
* status locally.
|
||||
*/
|
||||
UserSchema.virtual('banned')
|
||||
.get(function() {
|
||||
return this.status.banned.status;
|
||||
})
|
||||
.set(function(status) {
|
||||
this.status.banned.status = status;
|
||||
this.status.banned.history.push({
|
||||
status,
|
||||
created_at: new Date()
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* suspended returns true when the user is currently suspended, and sets the
|
||||
* suspension status locally.
|
||||
*/
|
||||
UserSchema.virtual('suspended')
|
||||
.get(function() {
|
||||
return Boolean(this.status.suspension.until && this.status.suspension.until > new Date());
|
||||
})
|
||||
.set(function(until) {
|
||||
this.status.suspension.until = until;
|
||||
this.status.suspension.history.push({
|
||||
until,
|
||||
created_at: new Date()
|
||||
});
|
||||
});
|
||||
|
||||
// Create the User model.
|
||||
const UserModel = mongoose.model('User', UserSchema);
|
||||
|
||||
|
||||
+42
-34
@@ -6,39 +6,30 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"postinstall": "./bin/cli plugins reconcile --skip-remote",
|
||||
"start": "./bin/cli serve -j -w",
|
||||
"dev-start": "nodemon -w . -w bin/cli -w bin/cli-serve --config .nodemon.json --exec \"yarn generate-introspection && ./bin/cli -c .env serve -j -w\"",
|
||||
"prebuild": "yarn generate-introspection",
|
||||
"build": "WEBPACK=TRUE NODE_ENV=production webpack -p --config webpack.config.js --bail",
|
||||
"prebuild-watch": "yarn generate-introspection",
|
||||
"build-watch": "WEBPACK=TRUE NODE_ENV=development webpack --progress --config webpack.config.js --watch",
|
||||
"lint": "yamllint locales/*.yml && eslint --ext=.js --ext=.json bin/* .",
|
||||
"lint-fix": "yarn lint --fix",
|
||||
"jest-watch": "TEST_MODE=unit NODE_ENV=test jest --watch",
|
||||
"e2e-ci": "./scripts/e2e-ci.sh",
|
||||
"generate-introspection": "WEBPACK=TRUE NODE_ENV=test ./scripts/generateIntrospectionResult.js",
|
||||
"clean": "rm -rf dist client/coral-framework/graphql/introspection.json",
|
||||
"watch": "npm-run-all clean generate-introspection --parallel watch:*",
|
||||
"watch:client": "NODE_ENV=development webpack --progress --watch",
|
||||
"watch:server": "nodemon --config .nodemon.json",
|
||||
"start:development": "NODE_ENV=development ./bin/cli -c .env serve -j -w",
|
||||
"start": "NODE_ENV=production ./bin/cli serve -j -w",
|
||||
"prebuild": "npm-run-all clean generate-introspection",
|
||||
"build": "NODE_ENV=production webpack -p --bail",
|
||||
"lint:yaml": "yamllint locales/*.yml",
|
||||
"lint:js": "eslint --ext=.js --ext=.json bin/* .",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"plugins:reconcile": "./bin/cli plugins reconcile",
|
||||
"test": "npm-run-all test:client test:server",
|
||||
"test:server": "TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
|
||||
"test:client": "TEST_MODE=unit NODE_ENV=test jest",
|
||||
"test:client:watch": "TEST_MODE=unit NODE_ENV=test jest --watch",
|
||||
"e2e": "./scripts/e2e.js",
|
||||
"test": "TEST_MODE=unit NODE_ENV=test jest && TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
|
||||
"test-cover": "TEST_MODE=unit NODE_ENV=test istanbul cover _mocha --report text --check-coverage -- -R spec",
|
||||
"heroku-postbuild": "./bin/cli plugins reconcile && yarn build",
|
||||
"generate-introspection": "WEBPACK=TRUE NODE_ENV=test ./scripts/generateIntrospectionResult.js"
|
||||
"e2e:ci": "./scripts/e2e-ci.sh",
|
||||
"heroku-postbuild": "npm-run-all plugins:reconcile build"
|
||||
},
|
||||
"talk": {
|
||||
"migration": {
|
||||
"minVersion": 1496771633
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"pre-git": {
|
||||
"commit-msg": [],
|
||||
"pre-commit": [
|
||||
"yarn lint",
|
||||
"yarn test"
|
||||
],
|
||||
"pre-push": [
|
||||
"yarn test"
|
||||
],
|
||||
"post-commit": [],
|
||||
"post-merge": []
|
||||
"minVersion": 1510174676
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
@@ -120,7 +111,6 @@
|
||||
"imports-loader": "^0.7.1",
|
||||
"inquirer": "^3.2.2",
|
||||
"ioredis": "3.1.4",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
"joi": "^10.6.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"jsonwebtoken": "^7.4.3",
|
||||
@@ -142,6 +132,7 @@
|
||||
"node-emoji": "^1.8.1",
|
||||
"node-fetch": "^1.7.2",
|
||||
"nodemailer": "^2.6.4",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"passport": "^0.4.0",
|
||||
"passport-jwt": "^3.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
@@ -170,7 +161,6 @@
|
||||
"redux": "^3.6.0",
|
||||
"redux-thunk": "^2.1.0",
|
||||
"resolve": "^1.4.0",
|
||||
"selenium-standalone": "^6.11.0",
|
||||
"semver": "^5.4.1",
|
||||
"simplemde": "^1.11.2",
|
||||
"smoothscroll-polyfill": "^0.3.5",
|
||||
@@ -197,6 +187,7 @@
|
||||
"browserstack-local": "^1.3.0",
|
||||
"chai": "^3.5.0",
|
||||
"chai-as-promised": "^6.0.0",
|
||||
"chai-datetime": "^1.5.0",
|
||||
"chai-http": "^3.0.0",
|
||||
"enzyme": "^3.0.0",
|
||||
"enzyme-adapter-react-15": "^1.0.0",
|
||||
@@ -208,7 +199,8 @@
|
||||
"mocha-junit-reporter": "^1.12.1",
|
||||
"nightwatch": "^0.9.16",
|
||||
"nodemon": "^1.11.0",
|
||||
"pre-git": "^3.15.3",
|
||||
"pre-git": "^3.16.0",
|
||||
"selenium-standalone": "^6.11.0",
|
||||
"sinon": "^3.2.1",
|
||||
"sinon-chai": "^2.13.0",
|
||||
"yaml-lint": "^1.0.0"
|
||||
@@ -216,8 +208,24 @@
|
||||
"engines": {
|
||||
"node": "^8"
|
||||
},
|
||||
"config": {
|
||||
"pre-git": {
|
||||
"pre-commit": [
|
||||
"yarn lint",
|
||||
"yarn test:client",
|
||||
"yarn test:server"
|
||||
],
|
||||
"pre-push": [
|
||||
"yarn lint",
|
||||
"yarn test:client",
|
||||
"yarn test:server"
|
||||
],
|
||||
"post-commit": [],
|
||||
"post-checkout": [],
|
||||
"post-merge": []
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"analyzeCommits": "simple-commit-message"
|
||||
},
|
||||
"snyk": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
module.exports = {
|
||||
|
||||
// mutations
|
||||
CREATE_COMMENT: 'CREATE_COMMENT',
|
||||
CREATE_ACTION: 'CREATE_ACTION',
|
||||
DELETE_ACTION: 'DELETE_ACTION',
|
||||
EDIT_NAME: 'EDIT_NAME',
|
||||
EDIT_COMMENT: 'EDIT_COMMENT',
|
||||
REJECT_USERNAME: 'REJECT_USERNAME',
|
||||
SET_USER_STATUS: 'SET_USER_STATUS',
|
||||
SUSPEND_USER: 'SUSPEND_USER',
|
||||
SET_COMMENT_STATUS: 'SET_COMMENT_STATUS',
|
||||
ADD_COMMENT_TAG: 'ADD_COMMENT_TAG',
|
||||
REMOVE_COMMENT_TAG: 'REMOVE_COMMENT_TAG',
|
||||
UPDATE_USER_ROLES: 'UPDATE_USER_ROLES',
|
||||
UPDATE_CONFIG: 'UPDATE_CONFIG',
|
||||
CREATE_TOKEN: 'CREATE_TOKEN',
|
||||
REVOKE_TOKEN: 'REVOKE_TOKEN',
|
||||
UPDATE_ASSET_SETTINGS: 'UPDATE_ASSET_SETTINGS',
|
||||
UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS',
|
||||
UPDATE_SETTINGS: 'UPDATE_SETTINGS',
|
||||
|
||||
// queries
|
||||
SEARCH_ASSETS: 'SEARCH_ASSETS',
|
||||
SEARCH_OTHER_USERS: 'SEARCH_OTHER_USERS',
|
||||
SEARCH_ACTIONS: 'SEARCH_ACTIONS',
|
||||
SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS: 'SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS',
|
||||
SEARCH_OTHERS_COMMENTS: 'SEARCH_OTHERS_COMMENTS',
|
||||
SEARCH_COMMENT_METRICS: 'SEARCH_COMMENT_METRICS',
|
||||
LIST_OWN_TOKENS: 'LIST_OWN_TOKENS',
|
||||
SEARCH_COMMENT_STATUS_HISTORY: 'SEARCH_COMMENT_STATUS_HISTORY',
|
||||
VIEW_SUSPENSION_INFO: 'VIEW_SUSPENSION_INFO',
|
||||
VIEW_PROTECTED_SETTINGS: 'VIEW_PROTECTED_SETTINGS',
|
||||
|
||||
// subscriptions
|
||||
SUBSCRIBE_COMMENT_ACCEPTED: 'SUBSCRIBE_COMMENT_ACCEPTED',
|
||||
SUBSCRIBE_COMMENT_REJECTED: 'SUBSCRIBE_COMMENT_REJECTED',
|
||||
SUBSCRIBE_COMMENT_FLAGGED: 'SUBSCRIBE_COMMENT_FLAGGED',
|
||||
SUBSCRIBE_ALL_COMMENT_ADDED: 'SUBSCRIBE_ALL_COMMENT_ADDED',
|
||||
SUBSCRIBE_ALL_COMMENT_EDITED: 'SUBSCRIBE_ALL_COMMENT_EDITED',
|
||||
SUBSCRIBE_ALL_USER_SUSPENDED: 'SUBSCRIBE_ALL_USER_SUSPENDED',
|
||||
SUBSCRIBE_ALL_USER_BANNED: 'SUBSCRIBE_ALL_USER_BANNED',
|
||||
SUBSCRIBE_ALL_USERNAME_REJECTED: 'SUBSCRIBE_ALL_USERNAME_REJECTED',
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
const merge = require('lodash/merge');
|
||||
|
||||
const mutation = require('./mutation');
|
||||
const query = require('./query');
|
||||
const subscription = require('./subscription');
|
||||
|
||||
module.exports = merge(...[
|
||||
mutation,
|
||||
query,
|
||||
subscription,
|
||||
]);
|
||||
@@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
CREATE_COMMENT: 'CREATE_COMMENT',
|
||||
CREATE_ACTION: 'CREATE_ACTION',
|
||||
CHANGE_USERNAME: 'CHANGE_USERNAME',
|
||||
SET_USERNAME: 'SET_USERNAME',
|
||||
DELETE_ACTION: 'DELETE_ACTION',
|
||||
EDIT_COMMENT: 'EDIT_COMMENT',
|
||||
SET_USER_USERNAME_STATUS: 'SET_USER_USERNAME_STATUS',
|
||||
SET_USER_BAN_STATUS: 'SET_USER_BAN_STATUS',
|
||||
SET_USER_SUSPENSION_STATUS: 'SET_USER_SUSPENSION_STATUS',
|
||||
SET_COMMENT_STATUS: 'SET_COMMENT_STATUS',
|
||||
ADD_COMMENT_TAG: 'ADD_COMMENT_TAG',
|
||||
REMOVE_COMMENT_TAG: 'REMOVE_COMMENT_TAG',
|
||||
UPDATE_USER_ROLES: 'UPDATE_USER_ROLES',
|
||||
UPDATE_CONFIG: 'UPDATE_CONFIG',
|
||||
CREATE_TOKEN: 'CREATE_TOKEN',
|
||||
REVOKE_TOKEN: 'REVOKE_TOKEN',
|
||||
UPDATE_ASSET_SETTINGS: 'UPDATE_ASSET_SETTINGS',
|
||||
UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS',
|
||||
UPDATE_SETTINGS: 'UPDATE_SETTINGS'
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
SEARCH_ASSETS: 'SEARCH_ASSETS',
|
||||
SEARCH_OTHER_USERS: 'SEARCH_OTHER_USERS',
|
||||
SEARCH_ACTIONS: 'SEARCH_ACTIONS',
|
||||
SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS: 'SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS',
|
||||
SEARCH_OTHERS_COMMENTS: 'SEARCH_OTHERS_COMMENTS',
|
||||
SEARCH_COMMENT_METRICS: 'SEARCH_COMMENT_METRICS',
|
||||
LIST_OWN_TOKENS: 'LIST_OWN_TOKENS',
|
||||
SEARCH_COMMENT_STATUS_HISTORY: 'SEARCH_COMMENT_STATUS_HISTORY',
|
||||
VIEW_SUSPENSION_INFO: 'VIEW_SUSPENSION_INFO',
|
||||
VIEW_PROTECTED_SETTINGS: 'VIEW_PROTECTED_SETTINGS',
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
SUBSCRIBE_COMMENT_ACCEPTED: 'SUBSCRIBE_COMMENT_ACCEPTED',
|
||||
SUBSCRIBE_COMMENT_REJECTED: 'SUBSCRIBE_COMMENT_REJECTED',
|
||||
SUBSCRIBE_COMMENT_FLAGGED: 'SUBSCRIBE_COMMENT_FLAGGED',
|
||||
SUBSCRIBE_ALL_COMMENT_ADDED: 'SUBSCRIBE_ALL_COMMENT_ADDED',
|
||||
SUBSCRIBE_ALL_COMMENT_EDITED: 'SUBSCRIBE_ALL_COMMENT_EDITED',
|
||||
SUBSCRIBE_ALL_USER_SUSPENDED: 'SUBSCRIBE_ALL_USER_SUSPENDED',
|
||||
SUBSCRIBE_ALL_USER_BANNED: 'SUBSCRIBE_ALL_USER_BANNED',
|
||||
SUBSCRIBE_ALL_USERNAME_REJECTED: 'SUBSCRIBE_ALL_USERNAME_REJECTED',
|
||||
SUBSCRIBE_ALL_USERNAME_APPROVED: 'SUBSCRIBE_ALL_USERNAME_APPROVED'
|
||||
};
|
||||
+22
-31
@@ -1,35 +1,28 @@
|
||||
const constants = require('./constants');
|
||||
const root = require('./rootReducer');
|
||||
const queries = require('./queryReducer');
|
||||
const mutations = require('./mutationReducer');
|
||||
const subscriptions = require('./subscriptionReducer');
|
||||
const reducers = require('./reducers');
|
||||
const constantsArray = Object.keys(constants);
|
||||
|
||||
const reducers = [
|
||||
root,
|
||||
queries,
|
||||
mutations,
|
||||
subscriptions,
|
||||
];
|
||||
/**
|
||||
* findGrant will try to check all the permissions if the user is allowed to do
|
||||
* so.
|
||||
*
|
||||
* @param {Object} user the user being checked whether they have the required
|
||||
* permissions
|
||||
* @param {Array<String>} perms the array of permissions that the user must have
|
||||
* in order to succeed
|
||||
*/
|
||||
const findGrant = (user, perms) => perms.every((perm) => {
|
||||
for (let key in reducers) {
|
||||
const reducer = reducers[key];
|
||||
const grant = reducer(user, perm);
|
||||
|
||||
// this will make 'reducer' a key in this array. hm.
|
||||
const allPermissions = Object.keys(constants);
|
||||
|
||||
const findGrant = (user, perms) => {
|
||||
|
||||
return perms.every((perm) => {
|
||||
|
||||
for (let key in reducers) {
|
||||
const reducer = reducers[key];
|
||||
const grant = reducer(user, perm);
|
||||
|
||||
if (grant !== null && typeof grant !== 'undefined') {
|
||||
return grant;
|
||||
}
|
||||
if (typeof grant !== 'undefined' && grant !== null) {
|
||||
return grant;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
return false;
|
||||
});
|
||||
|
||||
/**
|
||||
* returns true, false, or null depending on whether the user has those permissions
|
||||
@@ -40,10 +33,8 @@ const findGrant = (user, perms) => {
|
||||
* @return {Boolean}
|
||||
*/
|
||||
module.exports = (user, ...perms) => {
|
||||
|
||||
// Make sure all the passed permissions are not typos.
|
||||
const missingPerms = perms.filter((perm) => !allPermissions.includes(perm));
|
||||
if (missingPerms.length > 0) {
|
||||
if (perms.some((perm) => !constantsArray.includes(perm))) {
|
||||
const missingPerms = perms.filter((perm) => !constantsArray.includes(perm));
|
||||
throw new Error(`${missingPerms.join(' ')} are not valid permissions.`);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
const mutation = require('./mutation');
|
||||
const query = require('./query');
|
||||
const subscription = require('./subscription');
|
||||
|
||||
module.exports = [
|
||||
(user /* , perm*/) => {
|
||||
|
||||
// If a user is banned or currently suspended, then they aren't allowed to
|
||||
// do anything.
|
||||
if (user.banned || user.suspended) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
query,
|
||||
mutation,
|
||||
subscription,
|
||||
];
|
||||
@@ -1,30 +1,42 @@
|
||||
const {check} = require('./utils');
|
||||
const types = require('./constants');
|
||||
const {check} = require('../utils');
|
||||
const types = require('../constants');
|
||||
|
||||
module.exports = (user, perm) => {
|
||||
switch (perm) {
|
||||
case types.CHANGE_USERNAME:
|
||||
return user.status.username.status === 'REJECTED';
|
||||
|
||||
case types.SET_USERNAME:
|
||||
return user.status.username.status === 'UNSET';
|
||||
|
||||
case types.CREATE_COMMENT:
|
||||
case types.CREATE_ACTION:
|
||||
case types.DELETE_ACTION:
|
||||
case types.EDIT_NAME:
|
||||
case types.EDIT_COMMENT:
|
||||
return true;
|
||||
|
||||
// Anyone can do these things if they aren't suspended, banned, or blocked
|
||||
// as they're editing their username.
|
||||
return !['UNSET', 'REJECTED'].includes(user.status.username.status);
|
||||
|
||||
case types.ADD_COMMENT_TAG:
|
||||
case types.REMOVE_COMMENT_TAG:
|
||||
return check(user, ['ADMIN', 'MODERATOR', 'STAFF']);
|
||||
|
||||
case types.UPDATE_USER_ROLES:
|
||||
case types.REJECT_USERNAME:
|
||||
case types.SET_USER_STATUS:
|
||||
case types.SUSPEND_USER:
|
||||
case types.SET_COMMENT_STATUS:
|
||||
case types.SET_USER_USERNAME_STATUS:
|
||||
case types.SET_USER_BAN_STATUS:
|
||||
case types.SET_USER_SUSPENSION_STATUS:
|
||||
case types.UPDATE_CONFIG:
|
||||
case types.UPDATE_SETTINGS:
|
||||
case types.UPDATE_ASSET_SETTINGS:
|
||||
case types.UPDATE_ASSET_STATUS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
|
||||
case types.CREATE_TOKEN:
|
||||
case types.REVOKE_TOKEN:
|
||||
return check(user, ['ADMIN']);
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -1,28 +1,22 @@
|
||||
const {check} = require('./utils');
|
||||
const types = require('./constants');
|
||||
const {check} = require('../utils');
|
||||
const types = require('../constants');
|
||||
|
||||
module.exports = (user, perm) => {
|
||||
switch (perm) {
|
||||
case types.SEARCH_ASSETS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SEARCH_OTHER_USERS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SEARCH_ACTIONS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SEARCH_OTHERS_COMMENTS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SEARCH_COMMENT_METRICS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.LIST_OWN_TOKENS:
|
||||
return check(user, ['ADMIN']);
|
||||
case types.SEARCH_COMMENT_STATUS_HISTORY:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.VIEW_SUSPENSION_INFO:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.VIEW_PROTECTED_SETTINGS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
|
||||
case types.LIST_OWN_TOKENS:
|
||||
return check(user, ['ADMIN']);
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -1,24 +1,19 @@
|
||||
const {check} = require('./utils');
|
||||
const types = require('./constants');
|
||||
const {check} = require('../utils');
|
||||
const types = require('../constants');
|
||||
|
||||
module.exports = (user, perm) => {
|
||||
switch (perm) {
|
||||
case types.SUBSCRIBE_COMMENT_FLAGGED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_COMMENT_ACCEPTED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_COMMENT_REJECTED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_ALL_COMMENT_EDITED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_ALL_COMMENT_ADDED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_ALL_USER_SUSPENDED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_ALL_USER_BANNED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_ALL_USERNAME_REJECTED:
|
||||
case types.SUBSCRIBE_ALL_USERNAME_APPROVED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
module.exports = (user /* , perm*/) => {
|
||||
|
||||
// this runs before everything
|
||||
if (
|
||||
user.status === 'BANNED' ||
|
||||
(user.suspension.until && user.suspension.until > new Date())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,11 @@
|
||||
const intersection = require('lodash/intersection');
|
||||
|
||||
/**
|
||||
* check will ensure that the user has the desired roles.
|
||||
*
|
||||
* @param {Object} user user being checked for roles
|
||||
* @param {Array<String>} roles roles to check that the user has
|
||||
*/
|
||||
const check = (user, roles) => {
|
||||
return intersection(roles, user.roles).length > 0;
|
||||
};
|
||||
|
||||
@@ -57,27 +57,23 @@ module.exports = {
|
||||
hooks: {
|
||||
RootMutation: {
|
||||
addTag: {
|
||||
async post(obj, {tag: {name, id, item_type}}, {user, mutators: {Comment}, pubsub}, info, result) {
|
||||
async post(obj, {tag: {name, id, item_type}}, {user, mutators: {Comment}, pubsub}, _info) {
|
||||
if (name === 'FEATURED' && item_type === 'COMMENTS') {
|
||||
const comment = await Comment.setStatus({id: id, status: 'ACCEPTED'});
|
||||
if (comment) {
|
||||
pubsub.publish('commentFeatured', {comment, user});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
},
|
||||
removeTag: {
|
||||
async post(obj, {tag: {name, id, item_type}}, {user, loaders: {Comments}, pubsub}, info, result) {
|
||||
async post(obj, {tag: {name, id, item_type}}, {user, loaders: {Comments}, pubsub}, _info) {
|
||||
if (name === 'FEATURED' && item_type === 'COMMENTS') {
|
||||
const comment = await Comments.get.load(id);
|
||||
if (comment) {
|
||||
pubsub.publish('commentUnfeatured', {comment, user});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -35,7 +35,7 @@ class FlagDetails extends Component {
|
||||
<ul className={styles.info}>
|
||||
{reasons.map((reason) =>
|
||||
<li key={reason} className={styles.lessDetail}>
|
||||
{reason} {summaries[reason].userFlagged && `(${summaries[reason].count})`}
|
||||
{t(`flags.reasons.comment.${reason.toLowerCase()}`)} {summaries[reason].userFlagged && `(${summaries[reason].count})`}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import styles from './UserFlagDetails.css';
|
||||
|
||||
class UserFlagDetails extends Component {
|
||||
@@ -25,7 +26,7 @@ class UserFlagDetails extends Component {
|
||||
{Object.keys(summaries)
|
||||
.map((reason) => (
|
||||
<li key={reason}>
|
||||
{reason} ({summaries[reason].count})
|
||||
{t(`flags.reasons.comment.${reason.toLowerCase()}`)} ({summaries[reason].count})
|
||||
<ul className={styles.subDetail}>
|
||||
{summaries[reason].actions.map((action) =>
|
||||
<li key={action.user.id}>
|
||||
|
||||
@@ -4,12 +4,20 @@ en:
|
||||
Are you sure? The language in this comment might violate our community guidelines.
|
||||
You can edit the comment or submit it for moderator review.
|
||||
talk-plugin-toxic-comments:
|
||||
unlikely: Unlikely
|
||||
highly_likely: Highly Likely
|
||||
possibly: Possibly
|
||||
likely: Likely
|
||||
toxic_comment: Toxic Comment
|
||||
unlikely: "Unlikely"
|
||||
highly_likely: "Highly Likely"
|
||||
possibly: "Possibly"
|
||||
likely: "Likely"
|
||||
toxic_comment: "Toxic Comment"
|
||||
still_toxic: |
|
||||
This edited comment might still violate our community guidelines.
|
||||
Our moderation team will review your comment shortly.
|
||||
flags:
|
||||
reasons:
|
||||
comment:
|
||||
toxic_comment: "Highly likely to be Toxic"
|
||||
es:
|
||||
flags:
|
||||
reasons:
|
||||
comment:
|
||||
toxic_comment: "Muy probable que sea tóxico"
|
||||
@@ -98,13 +98,4 @@ router.put('/password/reset', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/username', authorization.needed(), async (req, res, next) => {
|
||||
try {
|
||||
await UsersService.editName(req.user.id, req.body.username);
|
||||
res.status(204).end();
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -4,11 +4,7 @@ const UsersService = require('../../../services/users');
|
||||
const mailer = require('../../../services/mailer');
|
||||
const errors = require('../../../errors');
|
||||
const authorization = require('../../../middleware/authorization');
|
||||
const i18n = require('../../../services/i18n');
|
||||
const Limit = require('../../../services/limit');
|
||||
const {
|
||||
ROOT_URL
|
||||
} = require('../../../config');
|
||||
|
||||
router.get('/', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => {
|
||||
|
||||
@@ -60,36 +56,6 @@ router.post('/:user_id/role', authorization.needed('ADMIN', 'MODERATOR'), async
|
||||
}
|
||||
});
|
||||
|
||||
// update the status of a user
|
||||
router.post('/:user_id/status', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => {
|
||||
let {status} = req.body;
|
||||
|
||||
try {
|
||||
let user = await UsersService.setStatus(req.params.user_id, status);
|
||||
if (!user) {
|
||||
return next(errors.ErrNotFound);
|
||||
}
|
||||
|
||||
if (user.status === 'BANNED') {
|
||||
req.pubsub.publish('userBanned', user);
|
||||
}
|
||||
|
||||
// TODO: investigate why this is returning a value? Also why is this a POST vs PUT?
|
||||
res.status(201).json(user.status);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:user_id/username-enable', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => {
|
||||
try {
|
||||
await UsersService.toggleNameEdit(req.params.user_id, true);
|
||||
res.status(204).end();
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:user_id/email', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => {
|
||||
try {
|
||||
let user = await UsersService.findById(req.params.user_id);
|
||||
@@ -114,26 +80,6 @@ router.post('/:user_id/email', authorization.needed('ADMIN', 'MODERATOR'), async
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* SendEmailConfirmation sends a confirmation email to the user.
|
||||
* @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 = async (user, email, referer) => {
|
||||
let token = await UsersService.createEmailConfirmToken(user, email, referer);
|
||||
|
||||
return mailer.sendSimple({
|
||||
template: 'email-confirm',
|
||||
locals: {
|
||||
token,
|
||||
rootURL: ROOT_URL,
|
||||
email
|
||||
},
|
||||
subject: i18n.t('email.confirm.subject'),
|
||||
to: email
|
||||
});
|
||||
};
|
||||
|
||||
// create a local user.
|
||||
router.post('/', async (req, res, next) => {
|
||||
const {email, password, username} = req.body;
|
||||
@@ -144,7 +90,7 @@ router.post('/', async (req, res, next) => {
|
||||
|
||||
// Send an email confirmation. The Front end will know about the
|
||||
// requireEmailConfirmation as it's included in the settings get endpoint.
|
||||
await SendEmailConfirmation(user, email, redirectUri);
|
||||
await UsersService.sendEmailConfirmation(user, email, redirectUri);
|
||||
|
||||
res.status(201).json(user);
|
||||
} catch (e) {
|
||||
@@ -152,26 +98,6 @@ router.post('/', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:user_id/actions', authorization.needed(), async (req, res, next) => {
|
||||
const {
|
||||
action_type,
|
||||
metadata
|
||||
} = req.body;
|
||||
|
||||
try {
|
||||
let action = await UsersService.addAction(req.params.user_id, req.user.id, action_type, metadata);
|
||||
|
||||
// Set the user status to "pending" for review by moderators
|
||||
if (action_type === 'FLAG') {
|
||||
await UsersService.setStatus(req.params.user_id, 'PENDING');
|
||||
}
|
||||
|
||||
res.status(201).json(action);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
});
|
||||
|
||||
// This will allow 1 try every minute.
|
||||
const resendRateLimiter = new Limit('/api/v1/users/resend-verify', 1, '1m');
|
||||
|
||||
@@ -205,7 +131,7 @@ router.post('/resend-verify', async (req, res, next) => {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
await SendEmailConfirmation(user, email, redirectUri);
|
||||
await UsersService.sendEmailConfirmation(user, email, redirectUri);
|
||||
|
||||
res.status(204).end();
|
||||
} catch (e) {
|
||||
@@ -234,7 +160,7 @@ router.post('/:user_id/email/confirm', authorization.needed('ADMIN', 'MODERATOR'
|
||||
}
|
||||
|
||||
// Send the email to the first local profile that was found.
|
||||
await SendEmailConfirmation(user, localProfile.id);
|
||||
await UsersService.sendEmailConfirmation(user, localProfile.id);
|
||||
|
||||
res.status(204).end();
|
||||
} catch (e) {
|
||||
|
||||
+45
-30
@@ -12,6 +12,7 @@ const {createGraphOptions} = require('../graph');
|
||||
const accepts = require('accepts');
|
||||
const apollo = require('graphql-server-express');
|
||||
const {DISABLE_STATIC_SERVER} = require('../config');
|
||||
const SetupService = require('../services/setup');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -19,29 +20,29 @@ const router = express.Router();
|
||||
// STATIC FILES
|
||||
//==============================================================================
|
||||
|
||||
// If the application is in production mode, then add gzip rewriting for the
|
||||
// content.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
router.get('*.js', (req, res, next) => {
|
||||
const accept = accepts(req);
|
||||
if (accept.encoding(['gzip']) === 'gzip') {
|
||||
|
||||
// Adjsut the headers on the request by adding a content type header
|
||||
// because express won't be able to detect the mime-type with the .gz
|
||||
// extension and we need to decalre support for the gzip encoding.
|
||||
res.set('Content-Type', 'application/javascript');
|
||||
res.set('Content-Encoding', 'gzip');
|
||||
|
||||
// Rewrite the url so that the gzip version will be served instead.
|
||||
req.url = `${req.url}.gz`;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
if (!DISABLE_STATIC_SERVER) {
|
||||
|
||||
// If the application is in production mode, then add gzip rewriting for the
|
||||
// content.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
router.get('*.js', (req, res, next) => {
|
||||
const accept = accepts(req);
|
||||
if (accept.encoding(['gzip']) === 'gzip') {
|
||||
|
||||
// Adjust the headers on the request by adding a content type header
|
||||
// because express won't be able to detect the mime-type with the .gz
|
||||
// extension and we need to declare support for the gzip encoding.
|
||||
res.set('Content-Type', 'application/javascript');
|
||||
res.set('Content-Encoding', 'gzip');
|
||||
|
||||
// Rewrite the url so that the gzip version will be served instead.
|
||||
req.url = `${req.url}.gz`;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve the directories under public/dist from this router.
|
||||
*/
|
||||
@@ -100,7 +101,7 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
});
|
||||
});
|
||||
|
||||
// GraphQL documention.
|
||||
// GraphQL documentation.
|
||||
router.get('/admin/docs', (req, res) => {
|
||||
res.render('admin/docs');
|
||||
});
|
||||
@@ -118,14 +119,28 @@ router.use('/embed', require('./embed'));
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
router.use('/assets', require('./assets'));
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
return res.render('article', {
|
||||
title: 'Coral Talk',
|
||||
asset_url: '',
|
||||
asset_id: '',
|
||||
body: '',
|
||||
basePath: '/client/embed/stream'
|
||||
});
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
await SetupService.isAvailable();
|
||||
return res.redirect('/admin/install');
|
||||
} catch (e) {
|
||||
return res.render('article', {
|
||||
title: 'Coral Talk',
|
||||
asset_url: '',
|
||||
asset_id: '',
|
||||
body: '',
|
||||
basePath: '/client/embed/stream'
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
await SetupService.isAvailable();
|
||||
return res.redirect('/admin/install');
|
||||
} catch (e) {
|
||||
return res.redirect('/admin');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ async function onListening() {
|
||||
let bind = typeof addr === 'string'
|
||||
? `pipe ${addr}`
|
||||
: `port ${addr.port}`;
|
||||
debug(`API Server Listening on ${bind}`);
|
||||
console.log(`API Server Listening on ${bind}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +141,7 @@ async function serve({jobs = true, websockets = true} = {}) {
|
||||
|
||||
// Mount the websocket server if requested.
|
||||
if (websockets) {
|
||||
debug(`Websocket Server Listening on ${port}`);
|
||||
console.log(`Websocket Server Listening on ${port}`);
|
||||
|
||||
// Mount the subscriptions server on the application server.
|
||||
createSubscriptionManager(server);
|
||||
|
||||
+58
-2
@@ -1,4 +1,7 @@
|
||||
const ActionModel = require('../models/action');
|
||||
const CommentModel = require('../models/comment');
|
||||
const UserModel = require('../models/user');
|
||||
const sc = require('snake-case');
|
||||
const _ = require('lodash');
|
||||
const errors = require('../errors');
|
||||
const events = require('./events');
|
||||
@@ -143,7 +146,7 @@ module.exports = class ActionsService {
|
||||
let $group = {
|
||||
|
||||
// group unique documents by these properties, we are leveraging the
|
||||
// fact that each uuid is completly unique.
|
||||
// fact that each uuid is completely unique.
|
||||
_id: {
|
||||
item_id: '$item_id',
|
||||
action_type: '$action_type',
|
||||
@@ -155,7 +158,7 @@ module.exports = class ActionsService {
|
||||
$sum: 1
|
||||
},
|
||||
|
||||
// we are leveraging the fact that each uuid is completly unique and
|
||||
// we are leveraging the fact that each uuid is completely unique and
|
||||
// just grabbing the last instance of the item type here.
|
||||
item_type: {
|
||||
$first: '$item_type'
|
||||
@@ -248,3 +251,56 @@ module.exports = class ActionsService {
|
||||
}, 'item_id');
|
||||
}
|
||||
};
|
||||
|
||||
const incrActionCounts = async (action, value) => {
|
||||
const ACTION_TYPE = sc(action.action_type.toLowerCase());
|
||||
|
||||
const update = {
|
||||
[`action_counts.${ACTION_TYPE}`]: value,
|
||||
};
|
||||
|
||||
if (action.group_id && action.group_id.length > 0) {
|
||||
const GROUP_ID = sc(action.group_id.toLowerCase());
|
||||
|
||||
update[`action_counts.${ACTION_TYPE}_${GROUP_ID}`] = value;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (action.item_type) {
|
||||
case 'USERS':
|
||||
return UserModel.update({
|
||||
id: action.item_id,
|
||||
}, {
|
||||
$inc: update,
|
||||
});
|
||||
case 'COMMENTS':
|
||||
return CommentModel.update({
|
||||
id: action.item_id,
|
||||
}, {
|
||||
$inc: update,
|
||||
});
|
||||
default:
|
||||
throw new Error('Invalid item type for action summary monitoring');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Can't mutate the action_counts.${ACTION_TYPE}:`, err);
|
||||
}
|
||||
};
|
||||
|
||||
// When a new action is created, modify the comment.
|
||||
events.on(ACTIONS_NEW, async (action) => {
|
||||
if (!action || (action.item_type !== 'COMMENTS' && action.item_type !== 'USERS')) {
|
||||
return;
|
||||
}
|
||||
|
||||
return incrActionCounts(action, 1);
|
||||
});
|
||||
|
||||
// When an action is deleted, remove the action count on the comment.
|
||||
events.on(ACTIONS_DELETE, async (action) => {
|
||||
if (!action || (action.item_type !== 'COMMENTS' && action.item_type !== 'USERS')) {
|
||||
return;
|
||||
}
|
||||
|
||||
return incrActionCounts(action, -1);
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
const CommentModel = require('../models/comment');
|
||||
const AssetModel = require('../models/asset');
|
||||
const SettingsService = require('./settings');
|
||||
const domainlist = require('./domainlist');
|
||||
const DomainList = require('./domain_list');
|
||||
const errors = require('../errors');
|
||||
const merge = require('lodash/merge');
|
||||
|
||||
@@ -64,7 +64,7 @@ module.exports = class AssetsService {
|
||||
|
||||
// Check the URL to confirm that is in the domain whitelist
|
||||
return Promise.all([
|
||||
domainlist.urlCheck(url),
|
||||
DomainList.urlCheck(url),
|
||||
SettingsService.retrieve()
|
||||
]).then(([whitelisted, settings]) => {
|
||||
|
||||
|
||||
@@ -4,13 +4,10 @@ const debug = require('debug')('talk:services:comments');
|
||||
const ActionsService = require('./actions');
|
||||
const SettingsService = require('./settings');
|
||||
|
||||
const sc = require('snake-case');
|
||||
const cloneDeep = require('lodash/cloneDeep');
|
||||
const errors = require('../errors');
|
||||
const events = require('./events');
|
||||
const {
|
||||
ACTIONS_NEW,
|
||||
ACTIONS_DELETE,
|
||||
COMMENTS_NEW,
|
||||
COMMENTS_EDIT,
|
||||
} = require('./events/constants');
|
||||
@@ -335,48 +332,6 @@ module.exports = class CommentsService {
|
||||
// Event Hooks
|
||||
//==============================================================================
|
||||
|
||||
const incrActionCounts = async (action, value) => {
|
||||
const ACTION_TYPE = sc(action.action_type.toLowerCase());
|
||||
|
||||
const update = {
|
||||
[`action_counts.${ACTION_TYPE}`]: value,
|
||||
};
|
||||
|
||||
if (action.group_id && action.group_id.length > 0) {
|
||||
const GROUP_ID = sc(action.group_id.toLowerCase());
|
||||
|
||||
update[`action_counts.${ACTION_TYPE}_${GROUP_ID}`] = value;
|
||||
}
|
||||
|
||||
try {
|
||||
await CommentModel.update({
|
||||
id: action.item_id,
|
||||
}, {
|
||||
$inc: update,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Can't mutate the action_counts.${ACTION_TYPE}:`, err);
|
||||
}
|
||||
};
|
||||
|
||||
// When a new action is created, modify the comment.
|
||||
events.on(ACTIONS_NEW, async (action) => {
|
||||
if (!action || action.item_type !== 'COMMENTS') {
|
||||
return;
|
||||
}
|
||||
|
||||
return incrActionCounts(action, 1);
|
||||
});
|
||||
|
||||
// When an action is deleted, remove the action count on the comment.
|
||||
events.on(ACTIONS_DELETE, async (action) => {
|
||||
if (!action || action.item_type !== 'COMMENTS') {
|
||||
return;
|
||||
}
|
||||
|
||||
return incrActionCounts(action, -1);
|
||||
});
|
||||
|
||||
const incrReplyCount = async (comment, value) => {
|
||||
try {
|
||||
await CommentModel.update({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const debug = require('debug')('talk:services:domainlist');
|
||||
const debug = require('debug')('talk:services:domain_list');
|
||||
const _ = require('lodash');
|
||||
const SettingsService = require('./settings');
|
||||
|
||||
@@ -8,7 +8,7 @@ const {ROOT_URL} = require('../config');
|
||||
* The root domainlist object.
|
||||
* @type {Object}
|
||||
*/
|
||||
class Domainlist {
|
||||
class DomainList {
|
||||
|
||||
constructor() {
|
||||
this.lists = {
|
||||
@@ -35,7 +35,7 @@ class Domainlist {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lists.whitelist = Domainlist.parseList(lists.whitelist);
|
||||
this.lists.whitelist = DomainList.parseList(lists.whitelist);
|
||||
debug(`Added ${lists.whitelist.length} domains to the whitelist.`);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class Domainlist {
|
||||
match(list, url) {
|
||||
|
||||
// Parse the url that we're matching with.
|
||||
const domainToMatch = Domainlist.parseURL(url);
|
||||
const domainToMatch = DomainList.parseURL(url);
|
||||
|
||||
// This will return true in the event that at least one blockword is found
|
||||
// in the phrase.
|
||||
@@ -61,7 +61,7 @@ class Domainlist {
|
||||
* @returns {Boolean} true if the domains match
|
||||
*/
|
||||
static matchMount(url) {
|
||||
return Domainlist.parseURL(url) === Domainlist.parseURL(ROOT_URL);
|
||||
return DomainList.parseURL(url) === DomainList.parseURL(ROOT_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +70,7 @@ class Domainlist {
|
||||
* @return {Array} the parsed list
|
||||
*/
|
||||
static parseList(list) {
|
||||
return _.uniq(list.map((domain) => Domainlist.parseURL(domain)));
|
||||
return _.uniq(list.map((domain) => DomainList.parseURL(domain)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +95,7 @@ class Domainlist {
|
||||
}
|
||||
|
||||
static async urlCheck(url) {
|
||||
const dl = new Domainlist();
|
||||
const dl = new DomainList();
|
||||
|
||||
// Load the domain list.
|
||||
await dl.load();
|
||||
@@ -106,4 +106,4 @@ class Domainlist {
|
||||
|
||||
}
|
||||
|
||||
module.exports = Domainlist;
|
||||
module.exports = DomainList;
|
||||
@@ -1,4 +1,9 @@
|
||||
module.exports.ACTIONS_DELETE = 'actions.delete';
|
||||
module.exports.ACTIONS_NEW = 'actions.new';
|
||||
module.exports.COMMENTS_NEW = 'comments.new';
|
||||
module.exports.COMMENTS_EDIT = 'comments.edit';
|
||||
module.exports = {
|
||||
ACTIONS_DELETE: 'ACTIONS_DELETE',
|
||||
ACTIONS_NEW: 'ACTIONS_NEW',
|
||||
COMMENTS_NEW: 'COMMENTS_NEW',
|
||||
COMMENTS_EDIT: 'COMMENTS_EDIT',
|
||||
USERS_SUSPENSION_CHANGE: 'USERS_SUSPENSION_CHANGE',
|
||||
USERS_BAN_CHANGE: 'USERS_BAN_CHANGE',
|
||||
USERS_USERNAME_STATUS_CHANGE: 'USERS_USERNAME_STATUS_CHANGE'
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
const ta = require('timeago.js');
|
||||
|
||||
ta.register('es', require('timeago.js/locales/es'));
|
||||
ta.register('da', require('timeago.js/locales/da'));
|
||||
ta.register('fr', require('timeago.js/locales/fr'));
|
||||
ta.register('pt_BR', require('timeago.js/locales/pt_BR'));
|
||||
|
||||
const timeago = ta();
|
||||
|
||||
module.exports = (time) => {
|
||||
return timeago.format(new Date(time), 'en');
|
||||
};
|
||||
+306
-255
@@ -1,8 +1,16 @@
|
||||
const assert = require('assert');
|
||||
const uuid = require('uuid');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const errors = require('../errors');
|
||||
const some = require('lodash/some');
|
||||
const merge = require('lodash/merge');
|
||||
const events = require('./events');
|
||||
const timeago = require('./timeago');
|
||||
|
||||
const {
|
||||
USERS_SUSPENSION_CHANGE,
|
||||
USERS_BAN_CHANGE,
|
||||
USERS_USERNAME_STATUS_CHANGE,
|
||||
} = require('./events/constants');
|
||||
|
||||
const {
|
||||
ROOT_URL
|
||||
@@ -15,7 +23,6 @@ const {
|
||||
const debug = require('debug')('talk:services:users');
|
||||
|
||||
const UserModel = require('../models/user');
|
||||
const USER_STATUS = require('../models/enum/user_status');
|
||||
const USER_ROLES = require('../models/enum/user_roles');
|
||||
|
||||
const RECAPTCHA_WINDOW = '10m'; // 10 minutes.
|
||||
@@ -23,8 +30,10 @@ const RECAPTCHA_INCORRECT_TRIGGER = 5; // after 3 incorrect attempts, recaptcha
|
||||
|
||||
const ActionsService = require('./actions');
|
||||
const MailerService = require('./mailer');
|
||||
const i18n = require('./i18n');
|
||||
const Wordlist = require('./wordlist');
|
||||
const Domainlist = require('./domainlist');
|
||||
const DomainList = require('./domain_list');
|
||||
const SettingsService = require('./settings');
|
||||
const {escapeRegExp} = require('./regex');
|
||||
|
||||
const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm';
|
||||
@@ -40,7 +49,7 @@ const loginRateLimiter = new Limit('loginAttempts', RECAPTCHA_INCORRECT_TRIGGER,
|
||||
|
||||
// UsersService is the interface for the application to interact with the
|
||||
// UserModel through.
|
||||
module.exports = class UsersService {
|
||||
class UsersService {
|
||||
|
||||
/**
|
||||
* Returns a user (if found) for the given email address.
|
||||
@@ -61,7 +70,7 @@ module.exports = class UsersService {
|
||||
}
|
||||
|
||||
/**
|
||||
* This records an unsucesfull login attempt for the given email address. If
|
||||
* This records an unsuccessful login attempt for the given email address. If
|
||||
* the maximum has been reached, the promise will be rejected with:
|
||||
*
|
||||
* errors.ErrLoginAttemptMaximumExceeded
|
||||
@@ -81,8 +90,190 @@ module.exports = class UsersService {
|
||||
}
|
||||
}
|
||||
|
||||
static async setSuspensionStatus(id, until, assignedBy = null) {
|
||||
let user = await UserModel.findOneAndUpdate({id}, {
|
||||
$set: {
|
||||
'status.suspension.until': until
|
||||
},
|
||||
$push: {
|
||||
'status.suspension.history': {
|
||||
until,
|
||||
assigned_by: assignedBy,
|
||||
created_at: Date.now()
|
||||
}
|
||||
}
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
if (user === null) {
|
||||
user = await UserModel.findOne({id});
|
||||
if (user === null) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
if (
|
||||
user.status.suspension.until === until ||
|
||||
(
|
||||
user.status.suspension.until.getTime() > until.getTime() - 1000 &&
|
||||
user.status.suspension.until.getTime() < until.getTime() + 1000
|
||||
)
|
||||
) {
|
||||
return user;
|
||||
}
|
||||
|
||||
throw new Error('suspension status change edit failed for an unknown reason');
|
||||
}
|
||||
|
||||
// Emit that the user username status was changed.
|
||||
await events.emitAsync(USERS_SUSPENSION_CHANGE, user, until);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
static async setBanStatus(id, status, assignedBy = null) {
|
||||
let user = await UserModel.findOneAndUpdate({
|
||||
id,
|
||||
status: {
|
||||
$ne: status
|
||||
}
|
||||
}, {
|
||||
$set: {
|
||||
'status.banned.status': status
|
||||
},
|
||||
$push: {
|
||||
'status.banned.history': {
|
||||
status,
|
||||
assigned_by: assignedBy,
|
||||
created_at: Date.now()
|
||||
}
|
||||
}
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
if (user === null) {
|
||||
user = await UserModel.findOne({id});
|
||||
if (user === null) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
if (user.status.banned.status === status) {
|
||||
return user;
|
||||
}
|
||||
|
||||
throw new Error('ban status change edit failed for an unknown reason');
|
||||
}
|
||||
|
||||
// Emit that the user ban status was changed.
|
||||
await events.emitAsync(USERS_BAN_CHANGE, user, status);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
static async setUsernameStatus(id, status, assignedBy = null) {
|
||||
let user = await UserModel.findOneAndUpdate({
|
||||
id,
|
||||
status: {
|
||||
$ne: status
|
||||
}
|
||||
}, {
|
||||
$set: {
|
||||
'status.username.status': status
|
||||
},
|
||||
$push: {
|
||||
'status.username.history': {
|
||||
status,
|
||||
assigned_by: assignedBy,
|
||||
created_at: Date.now()
|
||||
}
|
||||
}
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
if (user === null) {
|
||||
user = await UserModel.findOne({id});
|
||||
if (user === null) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
if (user.status.username.status === status) {
|
||||
return user;
|
||||
}
|
||||
|
||||
throw new Error('username status change edit failed for an unknown reason');
|
||||
}
|
||||
|
||||
// Emit that the user username status was changed.
|
||||
await events.emitAsync(USERS_USERNAME_STATUS_CHANGE, user, status);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
static async _setUsername(id, username, fromStatus, toStatus, resetAllowed = false) {
|
||||
try {
|
||||
const query = {
|
||||
id,
|
||||
'status.username.status': fromStatus
|
||||
};
|
||||
if (!resetAllowed) {
|
||||
query.username = {$ne: username};
|
||||
}
|
||||
|
||||
let user = await UserModel.findOneAndUpdate(query, {
|
||||
$set: {
|
||||
username,
|
||||
lowercaseUsername: username.toLowerCase(),
|
||||
'status.username.status': toStatus,
|
||||
},
|
||||
$push: {
|
||||
'status.username.history': {
|
||||
status: toStatus,
|
||||
assigned_by: id,
|
||||
created_at: Date.now()
|
||||
}
|
||||
}
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
if (!user) {
|
||||
user = await UsersService.findById(id);
|
||||
if (user === null) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
if (user.status.username.status !== fromStatus) {
|
||||
throw errors.ErrPermissionUpdateUsername;
|
||||
}
|
||||
|
||||
if (!resetAllowed && user.username === username) {
|
||||
throw errors.ErrSameUsernameProvided;
|
||||
}
|
||||
|
||||
throw new Error('edit username failed for an unexpected reason');
|
||||
}
|
||||
|
||||
// Emit that the user username status was changed.
|
||||
await events.emitAsync(USERS_USERNAME_STATUS_CHANGE, user, toStatus);
|
||||
|
||||
return user;
|
||||
} catch (err) {
|
||||
if (err.code === 11000) {
|
||||
throw errors.ErrUsernameTaken;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
static async setUsername(id, username) {
|
||||
return UsersService._setUsername(id, username, 'UNSET', 'SET', true);
|
||||
}
|
||||
|
||||
static async changeUsername(id, username) {
|
||||
return UsersService._setUsername(id, username, 'REJECTED', 'CHANGED');
|
||||
}
|
||||
|
||||
/**
|
||||
* This checks to see if the current login attempts against a user exeeds the
|
||||
* This checks to see if the current login attempts against a user exceeds the
|
||||
* maximum value allowed, if so, it rejects with:
|
||||
*
|
||||
* errors.ErrLoginAttemptMaximumExceeded
|
||||
@@ -181,13 +372,55 @@ module.exports = class UsersService {
|
||||
lowercaseUsername: username.toLowerCase(),
|
||||
roles: [],
|
||||
profiles: [{id, provider}],
|
||||
canEditName: true
|
||||
status: {
|
||||
username: {
|
||||
status: 'UNSET',
|
||||
history: {
|
||||
status: 'UNSET'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return user.save();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* sendEmailConfirmation sends a confirmation email to the user.
|
||||
* @param {String} user the user to send the email to
|
||||
* @param {String} email the email for the user to send the email to
|
||||
*/
|
||||
static async sendEmailConfirmation(user, email, redirectURI = ROOT_URL) {
|
||||
let token = await UsersService.createEmailConfirmToken(user, email, redirectURI);
|
||||
|
||||
return MailerService.sendSimple({
|
||||
template: 'email-confirm',
|
||||
locals: {
|
||||
token,
|
||||
rootURL: ROOT_URL,
|
||||
email
|
||||
},
|
||||
subject: i18n.t('email.confirm.subject'),
|
||||
to: email
|
||||
});
|
||||
}
|
||||
|
||||
static async sendEmail(user, options) {
|
||||
const localProfile = user.profiles.find((profile) => profile.provider === 'local');
|
||||
if (!localProfile) {
|
||||
throw new Error('user does not have an email');
|
||||
}
|
||||
|
||||
const {id: to} = localProfile;
|
||||
|
||||
options = merge(options, {
|
||||
to,
|
||||
});
|
||||
|
||||
return MailerService.sendSimple(options);
|
||||
}
|
||||
|
||||
static async changePassword(id, password) {
|
||||
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
|
||||
|
||||
@@ -289,7 +522,15 @@ module.exports = class UsersService {
|
||||
id: email,
|
||||
provider: 'local'
|
||||
}
|
||||
]
|
||||
],
|
||||
status: {
|
||||
username: {
|
||||
status: 'SET',
|
||||
history: {
|
||||
status: 'SET'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -307,36 +548,6 @@ module.exports = class UsersService {
|
||||
return 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
|
||||
}, {
|
||||
$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
|
||||
}, {
|
||||
$set: {
|
||||
disabled: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a role to a user.
|
||||
* @param {String} id id of a user
|
||||
@@ -380,131 +591,6 @@ module.exports = class UsersService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 async 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.
|
||||
throw new Error(`status ${status} is not supported`);
|
||||
}
|
||||
|
||||
// TODO: current updating status behavior is weird.
|
||||
// once a user has been `APPROVED` its status cannot be
|
||||
// changed anymore.
|
||||
const user = await UserModel.findOneAndUpdate({
|
||||
id,
|
||||
status: {
|
||||
$ne: 'APPROVED'
|
||||
}
|
||||
}, {
|
||||
$set: {
|
||||
status
|
||||
}
|
||||
}, {
|
||||
new: true,
|
||||
});
|
||||
|
||||
if (status === 'BANNED') {
|
||||
let localProfile = user.profiles.find((profile) => profile.provider === 'local');
|
||||
if (localProfile) {
|
||||
const options =
|
||||
{
|
||||
template: 'banned', // needed to know which template to render!
|
||||
locals: { // specifies the template locals.
|
||||
body: 'In accordance with The Coral Project’s community guidelines, your account has been banned. You are now longer allowed to comment, flag or engage with our community.'
|
||||
},
|
||||
subject: 'Your account has been banned',
|
||||
to: localProfile.id // This only works if the user has registered via e-mail.
|
||||
// We may want a standard way to access a user's e-mail address in the future
|
||||
};
|
||||
await MailerService.sendSimple(options);
|
||||
}
|
||||
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend a user until specified time.
|
||||
* @param {String} id id of a user
|
||||
* @param {String} message message to be send to the user
|
||||
* @param {Date} until date until the suspension is valid.
|
||||
*/
|
||||
static async suspendUser(id, message, until) {
|
||||
const user = await UserModel.findOneAndUpdate({id}, {
|
||||
$set: {
|
||||
suspension: {
|
||||
until,
|
||||
},
|
||||
}
|
||||
}, {
|
||||
new: true,
|
||||
});
|
||||
|
||||
if (message) {
|
||||
let localProfile = user.profiles.find((profile) => profile.provider === 'local');
|
||||
if (localProfile) {
|
||||
const options =
|
||||
{
|
||||
template: 'suspension', // needed to know which template to render!
|
||||
locals: { // specifies the template locals.
|
||||
body: message
|
||||
},
|
||||
subject: 'Your account has been suspended',
|
||||
to: localProfile.id // This only works if the user has registered via e-mail.
|
||||
// We may want a standard way to access a user's e-mail address in the future
|
||||
};
|
||||
|
||||
await MailerService.sendSimple(options);
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject username. It changes the status to BANNED and canEditName to True.
|
||||
* @param {String} id id of a user
|
||||
* @param {String} message message to be send to the user
|
||||
* @param {Date} until date until the suspension is valid.
|
||||
*/
|
||||
static async rejectUsername(id, message) {
|
||||
const user = await UserModel.findOneAndUpdate({id}, {
|
||||
$set: {
|
||||
status: 'BANNED',
|
||||
canEditName: true,
|
||||
}
|
||||
}, {
|
||||
new: true,
|
||||
});
|
||||
|
||||
if (message) {
|
||||
let localProfile = user.profiles.find(({provider}) => provider === 'local');
|
||||
if (localProfile) {
|
||||
const options = {
|
||||
template: 'suspension', // needed to know which template to render!
|
||||
locals: { // specifies the template locals.
|
||||
body: message
|
||||
},
|
||||
subject: 'Email Suspension',
|
||||
to: localProfile.id // This only works if the user has registered via e-mail.
|
||||
// We may want a standard way to access a user's e-mail address in the future
|
||||
};
|
||||
|
||||
await MailerService.sendSimple(options);
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a user with the id.
|
||||
* @param {String} id user id (uuid)
|
||||
@@ -567,7 +653,7 @@ module.exports = class UsersService {
|
||||
|
||||
const [user, domainValidated] = await Promise.all([
|
||||
UserModel.findOne({profiles: {$elemMatch: {id: email}}}),
|
||||
Domainlist.urlCheck(loc),
|
||||
DomainList.urlCheck(loc),
|
||||
]);
|
||||
if (!user) {
|
||||
|
||||
@@ -579,7 +665,7 @@ module.exports = class UsersService {
|
||||
|
||||
// If the domain didn't match any of the whitelisted domains and if it
|
||||
// didn't match the mount domain, then throw an error.
|
||||
if (!domainValidated && !Domainlist.matchMount(loc)) {
|
||||
if (!domainValidated && !DomainList.matchMount(loc)) {
|
||||
throw new Error('user supplied location exists on non-permitted domain');
|
||||
}
|
||||
|
||||
@@ -727,7 +813,7 @@ module.exports = class UsersService {
|
||||
*/
|
||||
static async createEmailConfirmToken(user, email, referer = ROOT_URL) {
|
||||
if (!email || typeof email !== 'string') {
|
||||
throw new Error('email is required when creating a JWT for resetting passord');
|
||||
throw new Error('email is required when creating a JWT for resetting password');
|
||||
}
|
||||
|
||||
// Conform the email to lowercase.
|
||||
@@ -791,97 +877,12 @@ module.exports = class UsersService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all users with pending 'ADMIN'ation actions.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static moderationQueue() {
|
||||
return UserModel.find({status: 'PENDING'});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives the user the ability to edit their username.
|
||||
* @param {String} id the id of the user to be toggled.
|
||||
* @param {Boolean} canEditName sets whether the user can edit their name.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static toggleNameEdit(id, canEditName) {
|
||||
return UserModel.update({id}, {
|
||||
$set: {canEditName}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the user's username.
|
||||
* @param {String} id The id of the user.
|
||||
* @param {String} username The new username for the user.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async editName(id, username) {
|
||||
|
||||
// TODO: Revisit this when we revamped User status workflows.
|
||||
const queryUsernameRejected = {
|
||||
id,
|
||||
username: {$ne: username},
|
||||
status: 'BANNED',
|
||||
canEditName: true
|
||||
};
|
||||
|
||||
const queryCreateUsername = {
|
||||
id,
|
||||
status: 'ACTIVE',
|
||||
canEditName: true
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await UserModel.findOneAndUpdate({
|
||||
$or: [queryUsernameRejected, queryCreateUsername],
|
||||
}, {
|
||||
$set: {
|
||||
username: username,
|
||||
lowercaseUsername: username.toLowerCase(),
|
||||
canEditName: false,
|
||||
status: 'PENDING',
|
||||
}
|
||||
}, {
|
||||
new: true,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
const user = await UsersService.findById(id);
|
||||
if (user === null) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
if (!user.canEditName) {
|
||||
throw errors.ErrPermissionUpdateUsername;
|
||||
}
|
||||
|
||||
if (user.username === username) {
|
||||
throw errors.ErrSameUsernameProvided;
|
||||
}
|
||||
|
||||
throw new Error('edit username failed for an unexpected reason');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch(err) {
|
||||
if (err.code === 11000) {
|
||||
throw errors.ErrUsernameTaken;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore another user
|
||||
* @param {String} id the id of the user that is ignoring another users
|
||||
* @param {Array<String>} usersToIgnore Array of user IDs to ignore
|
||||
*/
|
||||
static async ignoreUsers(id, usersToIgnore) {
|
||||
assert(Array.isArray(usersToIgnore), 'usersToIgnore is an array');
|
||||
assert(usersToIgnore.every((u) => typeof u === 'string'), 'usersToIgnore is an array of string user IDs');
|
||||
if (usersToIgnore.includes(id)) {
|
||||
throw new Error('Users cannot ignore themselves');
|
||||
}
|
||||
@@ -891,7 +892,6 @@ module.exports = class UsersService {
|
||||
throw errors.ErrCannotIgnoreStaff;
|
||||
}
|
||||
|
||||
// TODO: For each usersToIgnore, make sure they exist?
|
||||
return UserModel.update({id}, {
|
||||
$addToSet: {
|
||||
ignoresUsers: {
|
||||
@@ -907,15 +907,66 @@ module.exports = class UsersService {
|
||||
* @param {Array<String>} usersToStopIgnoring Array of user IDs to stop ignoring
|
||||
*/
|
||||
static async stopIgnoringUsers(id, usersToStopIgnoring) {
|
||||
assert(Array.isArray(usersToStopIgnoring), 'usersToStopIgnoring is an array');
|
||||
assert(usersToStopIgnoring.every((u) => typeof u === 'string'), 'usersToStopIgnoring is an array of string user IDs');
|
||||
await UserModel.update({id}, {
|
||||
$pullAll: {
|
||||
ignoresUsers: usersToStopIgnoring
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = UsersService;
|
||||
|
||||
events.on(USERS_BAN_CHANGE, async (user, status) => {
|
||||
|
||||
// Check to see if the user was banned now and is currently banned.
|
||||
if (user.banned && status) {
|
||||
await UsersService.sendEmail(user, {
|
||||
template: 'banned',
|
||||
locals: {
|
||||
body: 'In accordance with The Coral Project’s community guidelines, your account has been banned. You are now longer allowed to comment, flag or engage with our community.'
|
||||
},
|
||||
subject: 'Your account has been banned',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
events.on(USERS_SUSPENSION_CHANGE, async (user, until) => {
|
||||
|
||||
// Check to see if the user was suspended now and is currently suspended.
|
||||
if (user.suspended && until !== null && until > Date.now()) {
|
||||
const {organizationName} = await SettingsService.retrieve();
|
||||
|
||||
const message = i18n.t(
|
||||
'suspenduser.email_message_suspend',
|
||||
user.username,
|
||||
organizationName,
|
||||
timeago(until),
|
||||
);
|
||||
|
||||
await UsersService.sendEmail(user, {
|
||||
template: 'suspension',
|
||||
locals: {
|
||||
body: message,
|
||||
},
|
||||
subject: 'Your account has been banned',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
events.on(USERS_USERNAME_STATUS_CHANGE, async (user, status) => {
|
||||
if (status === 'REJECTED') {
|
||||
const message = i18n.t('reject_username.email_message_reject');
|
||||
|
||||
await UsersService.sendEmail(user, {
|
||||
template: 'suspension',
|
||||
locals: {
|
||||
body: message
|
||||
},
|
||||
subject: 'Username Rejected'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Extract all the tokenUserNotFound plugins so we can integrate with other
|
||||
// providers.
|
||||
|
||||
@@ -26,7 +26,6 @@ beforeEach(async () => {
|
||||
}));
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
after(async function() {
|
||||
mongoose.disconnect();
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
process.on('unhandledRejection', function(reason, promise) {
|
||||
console.error(promise);
|
||||
});
|
||||
@@ -48,7 +48,7 @@ describe('graph.mutations.addTag', () => {
|
||||
Object.entries({
|
||||
'anonymous': undefined,
|
||||
'regular commenter': new UserModel({}),
|
||||
'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'})
|
||||
'banned moderator': new UserModel({roles: ['MODERATOR'], banned: true})
|
||||
}).forEach(([ userDescription, user ]) => {
|
||||
it(userDescription, async () => {
|
||||
const context = new Context({user});
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const UsersService = require('../../../../services/users');
|
||||
|
||||
const {expect} = require('chai');
|
||||
|
||||
describe('graph.mutations.changeUsername', () => {
|
||||
let user;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
user = await UsersService.createLocalUser('test@test.com', 'testpassword1!', 'kirk');
|
||||
|
||||
expect(user).to.have.property('username', 'kirk');
|
||||
expect(user).to.have.property('lowercaseUsername', 'kirk');
|
||||
expect(user.status.username.status).to.equal('SET');
|
||||
});
|
||||
|
||||
const changeUsernameMutation = `
|
||||
mutation ChangeUsername($user_id: ID!, $username: String!) {
|
||||
changeUsername(id: $user_id, username: $username) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
[
|
||||
{roles: null},
|
||||
{roles: ['STAFF']},
|
||||
{roles: []},
|
||||
{roles: ['MODERATOR']},
|
||||
{roles: ['ADMIN']},
|
||||
{roles: ['ADMIN', 'MODERATOR']},
|
||||
].forEach(({roles}) => {
|
||||
it(`can change the username with roles ${roles && roles.length ? roles : JSON.stringify(roles)}`, async () => {
|
||||
let username = 'spock';
|
||||
|
||||
let ctx = new Context({user});
|
||||
|
||||
let res = await graphql(schema, changeUsernameMutation, {}, ctx, {
|
||||
user_id: user.id,
|
||||
username,
|
||||
});
|
||||
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
console.error(res.errors);
|
||||
}
|
||||
|
||||
expect(res.errors).to.be.undefined;
|
||||
expect(res.data.changeUsername).to.have.property('errors');
|
||||
expect(res.data.changeUsername.errors).to.have.length(1);
|
||||
expect(res.data.changeUsername.errors[0]).to.have.property('translation_key', 'NOT_AUTHORIZED');
|
||||
|
||||
// Set the user to the desired status.
|
||||
user = await UsersService.setUsernameStatus(user.id, 'REJECTED');
|
||||
|
||||
expect(user.status.username.status, 'REJECTED');
|
||||
|
||||
ctx = new Context({user});
|
||||
|
||||
res = await graphql(schema, changeUsernameMutation, {}, ctx, {
|
||||
user_id: user.id,
|
||||
username,
|
||||
});
|
||||
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
console.error(res.errors);
|
||||
}
|
||||
|
||||
expect(res.errors).to.be.undefined;
|
||||
expect(res.data.changeUsername).to.be.null;
|
||||
|
||||
user = await UsersService.findById(user.id);
|
||||
|
||||
expect(user.status.username.status, 'CHANGED');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -71,27 +71,28 @@ describe('graph.mutations.createComment', () => {
|
||||
beforeEach(() => AssetModel.create({id: '123'}));
|
||||
|
||||
[
|
||||
{user: new UserModel({status: 'ACTIVE'}), error: null},
|
||||
{user: new UserModel({status: 'BANNED'}), error: 'NOT_AUTHORIZED'},
|
||||
{user: new UserModel({status: 'PENDING'}), error: null},
|
||||
{user: new UserModel({status: 'APPROVED'}), error: null}
|
||||
{user: new UserModel({}), error: null},
|
||||
{user: new UserModel({banned: true}), error: 'NOT_AUTHORIZED'},
|
||||
{user: new UserModel({suspended: new Date((new Date()).getTime() - (10 * 86400000))}), error: null},
|
||||
{user: new UserModel({suspended: new Date((new Date()).getTime() + (10 * 86400000))}), error: 'NOT_AUTHORIZED'},
|
||||
].forEach(({user, error}) => {
|
||||
describe(`user.status=${user.status}`, () => {
|
||||
it(error ? 'does not create the comment' : 'creates the comment', () => {
|
||||
describe(`user.banned=${user.banned} user.suspended=${user.suspended}`, () => {
|
||||
it(error ? 'does not create the comment' : 'creates the comment', async () => {
|
||||
const context = new Context({user});
|
||||
const {data, errors} = await graphql(schema, query, {}, context);
|
||||
|
||||
return graphql(schema, query, {}, context)
|
||||
.then(({data, errors}) => {
|
||||
expect(errors).to.be.undefined;
|
||||
if (error) {
|
||||
expect(data.createComment).to.have.property('comment').null;
|
||||
expect(data.createComment).to.have.property('errors').not.null;
|
||||
expect(data.createComment.errors[0]).to.have.property('translation_key', error);
|
||||
} else {
|
||||
expect(data.createComment).to.have.property('comment').not.null;
|
||||
expect(data.createComment).to.have.property('errors').null;
|
||||
}
|
||||
});
|
||||
expect(errors).to.be.undefined;
|
||||
if (error) {
|
||||
expect(data.createComment).to.have.property('comment').null;
|
||||
expect(data.createComment).to.have.property('errors').not.null;
|
||||
expect(data.createComment.errors[0]).to.have.property('translation_key', error);
|
||||
} else {
|
||||
if (data.createComment.errors && data.createComment.errors.length > 0) {
|
||||
console.error(data.createComment.errors);
|
||||
}
|
||||
expect(data.createComment).to.have.property('errors').null;
|
||||
expect(data.createComment).to.have.property('comment').not.null;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -109,7 +110,7 @@ describe('graph.mutations.createComment', () => {
|
||||
beforeEach(() => asset.save());
|
||||
|
||||
it(error ? 'does not create the comment' : 'creates the comment', () => {
|
||||
const context = new Context({user: new UserModel({status: 'ACTIVE'})});
|
||||
const context = new Context({user: new UserModel({})});
|
||||
|
||||
return graphql(schema, query, {}, context)
|
||||
.then(({data, errors}) => {
|
||||
@@ -142,7 +143,7 @@ describe('graph.mutations.createComment', () => {
|
||||
beforeEach(() => AssetModel.create({id: '123', settings: {moderation}}));
|
||||
|
||||
it(`creates comment with status=${status}`, () => {
|
||||
const context = new Context({user: new UserModel({status: 'ACTIVE'})});
|
||||
const context = new Context({user: new UserModel()});
|
||||
|
||||
return graphql(schema, query, {}, context)
|
||||
.then(({data, errors}) => {
|
||||
@@ -172,33 +173,30 @@ describe('graph.mutations.createComment', () => {
|
||||
].forEach(({message, body, status, flagged}) => {
|
||||
describe(message, () => {
|
||||
|
||||
it(`should create a comment with status=${status} and it ${flagged ? 'should' : 'should not'} be flagged`, () => {
|
||||
const context = new Context({user: new UserModel({status: 'ACTIVE'})});
|
||||
it(`should create a comment with status=${status} and it ${flagged ? 'should' : 'should not'} be flagged`, async () => {
|
||||
const context = new Context({user: new UserModel({})});
|
||||
|
||||
return graphql(schema, query, {}, context, {
|
||||
const {data, errors} = await graphql(schema, query, {}, context, {
|
||||
input: {
|
||||
asset_id: '123',
|
||||
body
|
||||
}
|
||||
})
|
||||
.then(({data, errors}) => {
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.createComment).to.have.property('comment').not.null;
|
||||
expect(data.createComment.comment).to.have.property('status', status);
|
||||
expect(data.createComment).to.have.property('errors').null;
|
||||
});
|
||||
|
||||
return ActionModel.find({
|
||||
item_id: data.createComment.comment.id,
|
||||
action_type: 'FLAG'
|
||||
});
|
||||
})
|
||||
.then((actions) => {
|
||||
if (flagged) {
|
||||
expect(actions).to.have.length(1);
|
||||
} else {
|
||||
expect(actions).to.have.length(0);
|
||||
}
|
||||
});
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.createComment).to.have.property('comment').not.null;
|
||||
expect(data.createComment.comment).to.have.property('status', status);
|
||||
expect(data.createComment).to.have.property('errors').null;
|
||||
|
||||
const actions = await ActionModel.find({
|
||||
item_id: data.createComment.comment.id,
|
||||
action_type: 'FLAG'
|
||||
});
|
||||
if (flagged) {
|
||||
expect(actions).to.have.length(1);
|
||||
} else {
|
||||
expect(actions).to.have.length(0);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -217,30 +215,26 @@ describe('graph.mutations.createComment', () => {
|
||||
].forEach(({roles, tag}) => {
|
||||
describe(`user.roles=${JSON.stringify(roles)}`, () => {
|
||||
|
||||
it(`creates comment ${tag ? `with tag=${tag}` : 'without tags'}`, () => {
|
||||
it(`creates comment ${tag ? `with tag=${tag}` : 'without tags'}`, async () => {
|
||||
const context = new Context({user: new UserModel({roles})});
|
||||
|
||||
return graphql(schema, query, {}, context)
|
||||
.then(({data, errors}) => {
|
||||
if (errors) {
|
||||
console.error(errors);
|
||||
}
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.createComment).to.have.property('comment').not.null;
|
||||
expect(data.createComment).to.have.property('errors').null;
|
||||
const {data, errors} = await graphql(schema, query, {}, context);
|
||||
|
||||
return CommentsService.findById(data.createComment.comment.id);
|
||||
})
|
||||
.then(({tags}) => {
|
||||
if (tag) {
|
||||
expect(tags).to.have.length(1);
|
||||
expect(tags[0].tag.name).to.have.equal(tag);
|
||||
} else {
|
||||
expect(tags).length(0);
|
||||
}
|
||||
});
|
||||
if (errors) {
|
||||
console.error(errors);
|
||||
}
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.createComment).to.have.property('comment').not.null;
|
||||
expect(data.createComment).to.have.property('errors').null;
|
||||
|
||||
const {tags} = await CommentsService.findById(data.createComment.comment.id);
|
||||
if (tag) {
|
||||
expect(tags).to.have.length(1);
|
||||
expect(tags[0].tag.name).to.have.equal(tag);
|
||||
} else {
|
||||
expect(tags).length(0);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('graph.mutations.removeTag', () => {
|
||||
Object.entries({
|
||||
'anonymous': undefined,
|
||||
'regular commenter': new UserModel({}),
|
||||
'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'})
|
||||
'banned moderator': new UserModel({roles: ['MODERATOR'], banned: true})
|
||||
}).forEach(([userDescription, user]) => {
|
||||
it(userDescription, async function () {
|
||||
const context = new Context({user});
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const UserModel = require('../../../../models/user');
|
||||
const UsersService = require('../../../../services/users');
|
||||
|
||||
const {expect} = require('chai');
|
||||
|
||||
describe('graph.mutations.setUserBanStatus', () => {
|
||||
let user;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
|
||||
user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA');
|
||||
});
|
||||
|
||||
const setUserBanStatusMutation = `
|
||||
mutation SetUserBanStatus($user_id: ID!, $status: Boolean!) {
|
||||
setUserBanStatus(input: {
|
||||
id: $user_id,
|
||||
status: $status
|
||||
}) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
[
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: null},
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: ['STAFF']},
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: []},
|
||||
{error: 'NOT_AUTHORIZED', roles: null},
|
||||
{error: 'NOT_AUTHORIZED', roles: ['STAFF']},
|
||||
{error: 'NOT_AUTHORIZED', roles: []},
|
||||
{error: false, roles: ['MODERATOR']},
|
||||
{error: false, roles: ['ADMIN']},
|
||||
{error: false, roles: ['ADMIN', 'MODERATOR']},
|
||||
].forEach(({self, error, roles}) => {
|
||||
it(`${error ? 'can not' : 'can'} ban ${self ? 'themself' : 'another user'} as a user with roles ${roles && roles.length ? roles : JSON.stringify(roles)}`, async () => {
|
||||
const actor = new UserModel({roles});
|
||||
|
||||
// If we're testing self assign, set the id of the actor to the user
|
||||
// we're acting on.
|
||||
if (self) {
|
||||
actor.id = user.id;
|
||||
}
|
||||
|
||||
const ctx = new Context({user: actor});
|
||||
|
||||
const {data, errors} = await graphql(schema, setUserBanStatusMutation, {}, ctx, {
|
||||
user_id: user.id,
|
||||
status: true
|
||||
});
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
console.error(errors);
|
||||
}
|
||||
expect(errors).to.be.undefined;
|
||||
if (error) {
|
||||
expect(data.setUserBanStatus).to.have.property('errors').not.null;
|
||||
expect(data.setUserBanStatus.errors[0]).to.have.property('translation_key', error);
|
||||
} else {
|
||||
expect(data.setUserBanStatus).to.be.null;
|
||||
|
||||
user = await UserModel.findOne({id: user.id});
|
||||
|
||||
expect(user.status.banned.status).to.be.true;
|
||||
expect(user.status.banned.history).to.have.length(1);
|
||||
expect(user.status.banned.history[0]).to.have.property('status', true);
|
||||
expect(user.status.banned.history[0]).to.have.property('assigned_by', actor.id);
|
||||
expect(user.status.banned.history[0]).to.have.property('created_at').not.null;
|
||||
|
||||
expect(user.banned).to.be.true;
|
||||
|
||||
const res = await graphql(schema, setUserBanStatusMutation, {}, ctx, {
|
||||
user_id: user.id,
|
||||
status: false
|
||||
});
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
console.error(res.errors);
|
||||
}
|
||||
expect(res.errors).to.be.undefined;
|
||||
expect(res.data.setUserBanStatus).to.be.null;
|
||||
|
||||
user = await UserModel.findOne({id: user.id});
|
||||
|
||||
expect(user.status.banned.status).to.be.false;
|
||||
expect(user.status.banned.history).to.have.length(2);
|
||||
expect(user.status.banned.history[0]).to.have.property('status').to.be.true;
|
||||
expect(user.status.banned.history[0]).to.have.property('assigned_by', actor.id);
|
||||
expect(user.status.banned.history[0]).to.have.property('created_at').not.null;
|
||||
expect(user.status.banned.history[1]).to.have.property('status').to.be.false;
|
||||
expect(user.status.banned.history[1]).to.have.property('assigned_by', actor.id);
|
||||
expect(user.status.banned.history[1]).to.have.property('created_at').not.null;
|
||||
|
||||
expect(user.banned).to.be.false;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
const {graphql} = require('graphql');
|
||||
const timekeeper = require('timekeeper');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const UserModel = require('../../../../models/user');
|
||||
const UsersService = require('../../../../services/users');
|
||||
|
||||
const chai = require('chai');
|
||||
chai.use(require('chai-datetime'));
|
||||
const {expect} = chai;
|
||||
|
||||
describe('graph.mutations.setUserSuspensionStatus', () => {
|
||||
let user;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
|
||||
user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA');
|
||||
});
|
||||
|
||||
const setUserSuspensionStatusMutation = `
|
||||
mutation SetUserUsernameStatus($user_id: ID!, $until: Date) {
|
||||
setUserSuspensionStatus(input: {
|
||||
id: $user_id,
|
||||
until: $until
|
||||
}) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
[
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: null},
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: ['STAFF']},
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: []},
|
||||
{error: 'NOT_AUTHORIZED', roles: null},
|
||||
{error: 'NOT_AUTHORIZED', roles: ['STAFF']},
|
||||
{error: 'NOT_AUTHORIZED', roles: []},
|
||||
{error: false, roles: ['MODERATOR']},
|
||||
{error: false, roles: ['ADMIN']},
|
||||
{error: false, roles: ['ADMIN', 'MODERATOR']},
|
||||
].forEach(({self, error, roles}) => {
|
||||
it(`${error ? 'can not' : 'can'} suspend ${self ? 'themself' : 'another user'} as a user with roles ${roles && roles.length ? roles : JSON.stringify(roles)}`, async () => {
|
||||
const actor = new UserModel({roles});
|
||||
|
||||
// If we're testing self assign, set the id of the actor to the user
|
||||
// we're acting on.
|
||||
if (self) {
|
||||
actor.id = user.id;
|
||||
}
|
||||
|
||||
const ctx = new Context({user: actor});
|
||||
|
||||
const now = new Date();
|
||||
const oneHourFromNow = new Date(new Date(now).setHours(now.getHours() + 1));
|
||||
|
||||
const {data, errors} = await graphql(schema, setUserSuspensionStatusMutation, {}, ctx, {
|
||||
user_id: user.id,
|
||||
until: oneHourFromNow
|
||||
});
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
console.error(errors);
|
||||
}
|
||||
expect(errors).to.be.undefined;
|
||||
if (error) {
|
||||
expect(data.setUserSuspensionStatus).to.have.property('errors').not.null;
|
||||
expect(data.setUserSuspensionStatus.errors[0]).to.have.property('translation_key', error);
|
||||
} else {
|
||||
expect(data.setUserSuspensionStatus).to.be.null;
|
||||
|
||||
user = await UserModel.findOne({id: user.id});
|
||||
|
||||
// Mongoose messes with the date, check within a 2 second window.
|
||||
expect(user.status.suspension.until).to.be.withinTime(new Date(oneHourFromNow.getTime() - 1000), new Date(oneHourFromNow.getTime() + 1000));
|
||||
expect(user.status.suspension.history).to.have.length(1);
|
||||
expect(user.status.suspension.history[0]).to.have.property('until').to.be.withinTime(new Date(oneHourFromNow.getTime() - 1000), new Date(oneHourFromNow.getTime() + 1000));
|
||||
expect(user.status.suspension.history[0]).to.have.property('assigned_by', actor.id);
|
||||
expect(user.status.suspension.history[0]).to.have.property('created_at').not.null;
|
||||
|
||||
expect(user.suspended).to.be.true;
|
||||
timekeeper.travel(new Date(oneHourFromNow.getTime() + 10000));
|
||||
expect(user.suspended).to.be.false;
|
||||
timekeeper.reset();
|
||||
|
||||
const res = await graphql(schema, setUserSuspensionStatusMutation, {}, ctx, {
|
||||
user_id: user.id,
|
||||
until: null
|
||||
});
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
console.error(res.errors);
|
||||
}
|
||||
expect(res.errors).to.be.undefined;
|
||||
expect(res.data.setUserSuspensionStatus).to.be.null;
|
||||
|
||||
user = await UserModel.findOne({id: user.id});
|
||||
|
||||
// Mongoose messes with the date, check within a 2 second window.
|
||||
expect(user.status.suspension.until).to.be.null;
|
||||
expect(user.status.suspension.history).to.have.length(2);
|
||||
expect(user.status.suspension.history[0]).to.have.property('until').to.be.withinTime(new Date(oneHourFromNow.getTime() - 1000), new Date(oneHourFromNow.getTime() + 1000));
|
||||
expect(user.status.suspension.history[0]).to.have.property('assigned_by', actor.id);
|
||||
expect(user.status.suspension.history[0]).to.have.property('created_at').not.null;
|
||||
expect(user.status.suspension.history[1]).to.have.property('until').to.be.null;
|
||||
expect(user.status.suspension.history[1]).to.have.property('assigned_by', actor.id);
|
||||
expect(user.status.suspension.history[1]).to.have.property('created_at').not.null;
|
||||
|
||||
expect(user.suspended).to.be.false;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const UserModel = require('../../../../models/user');
|
||||
const UsersService = require('../../../../services/users');
|
||||
|
||||
const chai = require('chai');
|
||||
chai.use(require('chai-datetime'));
|
||||
const {expect} = chai;
|
||||
|
||||
[
|
||||
{status: 'APPROVED', name: 'approve', mutation: 'approveUsername'},
|
||||
{status: 'REJECTED', name: 'reject', mutation: 'rejectUsername'}
|
||||
].forEach(({status, name, mutation}) => {
|
||||
describe(`graph.mutations.${mutation}`, () => {
|
||||
let user;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
|
||||
user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA');
|
||||
});
|
||||
|
||||
const setUserUsernameStatusMutation = `
|
||||
mutation SetUserUsernameStatus($user_id: ID!) {
|
||||
${mutation}(id: $user_id) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
[
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: null},
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: ['STAFF']},
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: []},
|
||||
{error: 'NOT_AUTHORIZED', roles: null},
|
||||
{error: 'NOT_AUTHORIZED', roles: ['STAFF']},
|
||||
{error: 'NOT_AUTHORIZED', roles: []},
|
||||
{error: false, roles: ['MODERATOR']},
|
||||
{error: false, roles: ['ADMIN']},
|
||||
{error: false, roles: ['ADMIN', 'MODERATOR']},
|
||||
].forEach(({self, error, roles}) => {
|
||||
it(`${error ? 'can not' : 'can'} ${name} a username with the user roles ${roles && roles.length ? roles : JSON.stringify(roles)}${self ? ' on themself' : ''}`, async () => {
|
||||
const actor = new UserModel({roles});
|
||||
|
||||
// If we're testing self assign, set the id of the actor to the user
|
||||
// we're acting on.
|
||||
if (self) {
|
||||
actor.id = user.id;
|
||||
}
|
||||
|
||||
const ctx = new Context({user: actor});
|
||||
|
||||
const {data, errors} = await graphql(schema, setUserUsernameStatusMutation, {}, ctx, {
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
console.error(errors);
|
||||
}
|
||||
expect(errors).to.be.undefined;
|
||||
if (error) {
|
||||
expect(data[mutation]).to.have.property('errors').not.null;
|
||||
expect(data[mutation].errors[0]).to.have.property('translation_key', error);
|
||||
} else {
|
||||
expect(data[mutation]).to.be.null;
|
||||
|
||||
user = await UserModel.findOne({id: user.id});
|
||||
|
||||
expect(user.status.username.status).to.equal(status);
|
||||
expect(user.status.username.history).to.have.length(2);
|
||||
expect(user.status.username.history[0]).to.have.property('status', 'SET');
|
||||
expect(user.status.username.history[0]).to.have.property('assigned_by').is.null;
|
||||
expect(user.status.username.history[0]).to.have.property('created_at').not.null;
|
||||
expect(user.status.username.history[1]).to.have.property('status', status);
|
||||
expect(user.status.username.history[1]).to.have.property('assigned_by', actor.id);
|
||||
expect(user.status.username.history[1]).to.have.property('created_at').not.null;
|
||||
|
||||
expect(user.status.username.history[1].created_at).afterTime(user.status.username.history[0].created_at);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,8 +22,7 @@ describe('graph.mutations.updateAssetSettings', () => {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}`;
|
||||
|
||||
describe('context with different user roles', () => {
|
||||
|
||||
|
||||
@@ -21,8 +21,7 @@ describe('graph.mutations.updateSettings', () => {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}`;
|
||||
|
||||
describe('context with different user roles', () => {
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
const migration = require('../../../migrations/1510174676_user_status');
|
||||
const UserModel = require('../../../models/user');
|
||||
|
||||
const chai = require('chai');
|
||||
chai.use(require('chai-datetime'));
|
||||
const {expect} = chai;
|
||||
|
||||
describe('migration.1510174676_user_status', () => {
|
||||
describe('active user', () => {
|
||||
beforeEach(async () => {
|
||||
await UserModel.collection.insert({
|
||||
id: '123',
|
||||
username: 'Kirk',
|
||||
lowercaseUsername: 'kirk',
|
||||
status: 'ACTIVE',
|
||||
canEditName: false
|
||||
});
|
||||
});
|
||||
|
||||
it('completes the migration', async () => {
|
||||
|
||||
let user = await UserModel.collection.findOne({id: '123'});
|
||||
|
||||
expect(user).to.have.property('status', 'ACTIVE');
|
||||
expect(user).to.have.property('canEditName', false);
|
||||
|
||||
// Perform the migration.
|
||||
await migration.up();
|
||||
|
||||
user = await UserModel.collection.findOne({id: '123'});
|
||||
|
||||
// Check that it was correct.
|
||||
expect(user).to.have.property('status');
|
||||
expect(user.status).to.have.property('username');
|
||||
expect(user.status.username).to.have.property('status', 'SET');
|
||||
expect(user.status.username.history).to.have.length(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('social user', () => {
|
||||
beforeEach(async () => {
|
||||
await UserModel.collection.insert({
|
||||
id: '123',
|
||||
username: 'Kirk',
|
||||
lowercaseUsername: 'kirk',
|
||||
status: 'ACTIVE',
|
||||
canEditName: true
|
||||
});
|
||||
});
|
||||
|
||||
it('completes the migration', async () => {
|
||||
|
||||
let user = await UserModel.collection.findOne({id: '123'});
|
||||
|
||||
expect(user).to.have.property('status', 'ACTIVE');
|
||||
expect(user).to.have.property('canEditName', true);
|
||||
|
||||
// Perform the migration.
|
||||
await migration.up();
|
||||
|
||||
user = await UserModel.collection.findOne({id: '123'});
|
||||
|
||||
// Check that it was correct.
|
||||
expect(user).to.have.property('status');
|
||||
expect(user.status).to.have.property('username');
|
||||
expect(user.status.username).to.have.property('status', 'UNSET');
|
||||
expect(user.status.username.history).to.have.length(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rejected username', () => {
|
||||
beforeEach(async () => {
|
||||
await UserModel.collection.insert({
|
||||
id: '123',
|
||||
username: 'Kirk',
|
||||
lowercaseUsername: 'kirk',
|
||||
status: 'BANNED',
|
||||
canEditName: true
|
||||
});
|
||||
});
|
||||
|
||||
it('completes the migration', async () => {
|
||||
|
||||
let user = await UserModel.collection.findOne({id: '123'});
|
||||
|
||||
expect(user).to.have.property('status');
|
||||
expect(user.status).to.equal('BANNED');
|
||||
expect(user.canEditName).to.equal(true);
|
||||
|
||||
// Perform the migration.
|
||||
await migration.up();
|
||||
|
||||
user = await UserModel.collection.findOne({id: '123'});
|
||||
|
||||
// Check that it was correct.
|
||||
expect(user).to.have.property('status');
|
||||
expect(user.status).to.have.property('banned');
|
||||
expect(user.status.banned).to.have.property('status', false);
|
||||
expect(user.status.username).to.have.property('status', 'REJECTED');
|
||||
expect(user.status.username.history).to.have.length(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('approved username', () => {
|
||||
beforeEach(async () => {
|
||||
await UserModel.collection.insert({
|
||||
id: '123',
|
||||
username: 'Kirk',
|
||||
lowercaseUsername: 'kirk',
|
||||
status: 'APPROVED',
|
||||
canEditName: false
|
||||
});
|
||||
});
|
||||
|
||||
it('completes the migration', async () => {
|
||||
|
||||
let user = await UserModel.collection.findOne({id: '123'});
|
||||
|
||||
expect(user).to.have.property('status');
|
||||
expect(user.status).to.equal('APPROVED');
|
||||
expect(user.canEditName).to.equal(false);
|
||||
|
||||
// Perform the migration.
|
||||
await migration.up();
|
||||
|
||||
user = await UserModel.collection.findOne({id: '123'});
|
||||
|
||||
// Check that it was correct.
|
||||
expect(user).to.have.property('status');
|
||||
expect(user.status).to.have.property('banned');
|
||||
expect(user.status.banned).to.have.property('status', false);
|
||||
expect(user.status.username).to.have.property('status', 'APPROVED');
|
||||
expect(user.status.username.history).to.have.length(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('suspended user', () => {
|
||||
beforeEach(async () => {
|
||||
await UserModel.collection.insert({
|
||||
id: '123',
|
||||
username: 'Kirk',
|
||||
lowercaseUsername: 'kirk',
|
||||
status: 'ACTIVE',
|
||||
suspension: {
|
||||
until: new Date()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('completes the migration', async () => {
|
||||
|
||||
let user = await UserModel.collection.findOne({id: '123'});
|
||||
|
||||
expect(user).to.have.property('suspension');
|
||||
expect(user.suspension).to.have.property('until');
|
||||
expect(user.suspension.until).to.not.be.null;
|
||||
|
||||
const until = user.suspension.until;
|
||||
|
||||
// Perform the migration.
|
||||
await migration.up();
|
||||
|
||||
user = await UserModel.collection.findOne({id: '123'});
|
||||
|
||||
// Check that it was correct.
|
||||
expect(user).to.have.property('status');
|
||||
expect(user.status).to.have.property('suspension');
|
||||
expect(user.status.suspension).to.have.property('until');
|
||||
expect(user.status.suspension.until).to.not.be.null;
|
||||
expect(user.status.suspension.until).to.be.withinTime(new Date(until.getTime() - 1000), new Date(until.getTime() + 1000));
|
||||
});
|
||||
});
|
||||
|
||||
describe('banned user', () => {
|
||||
beforeEach(async () => {
|
||||
await UserModel.collection.insert({
|
||||
id: '123',
|
||||
username: 'Kirk',
|
||||
lowercaseUsername: 'kirk',
|
||||
status: 'BANNED',
|
||||
canEditName: false
|
||||
});
|
||||
});
|
||||
|
||||
it('completes the migration', async () => {
|
||||
|
||||
let user = await UserModel.collection.findOne({id: '123'});
|
||||
|
||||
expect(user).to.have.property('status');
|
||||
expect(user.status).to.equal('BANNED');
|
||||
|
||||
// Perform the migration.
|
||||
await migration.up();
|
||||
|
||||
user = await UserModel.collection.findOne({id: '123'});
|
||||
|
||||
// Check that it was correct.
|
||||
expect(user).to.have.property('status');
|
||||
expect(user.status).to.have.property('banned');
|
||||
expect(user.status.banned).to.have.property('status', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
const passport = require('../../../passport');
|
||||
|
||||
const app = require('../../../../../app');
|
||||
|
||||
const UsersService = require('../../../../../services/users');
|
||||
const SettingsService = require('../../../../../services/settings');
|
||||
const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
|
||||
|
||||
const chai = require('chai');
|
||||
chai.should();
|
||||
chai.use(require('chai-http'));
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('/api/v1/account/username', () => {
|
||||
let mockUser;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init(settings);
|
||||
mockUser = await UsersService.createLocalUser('ana@gmail.com', '123321123', 'Ana');
|
||||
});
|
||||
|
||||
describe('#put', () => {
|
||||
it('it should enable a user to edit their username if canEditName is enabled', async () => {
|
||||
await chai.request(app)
|
||||
.post(`/api/v1/users/${mockUser.id}/username-enable`)
|
||||
.set(passport.inject({id: '456', roles: ['ADMIN']}));
|
||||
|
||||
const res = await chai.request(app)
|
||||
.put('/api/v1/account/username')
|
||||
.set(passport.inject({id: mockUser.id, roles: []}))
|
||||
.send({username: 'MojoJojo'});
|
||||
|
||||
expect(res).to.have.status(204);
|
||||
});
|
||||
|
||||
it('it should return an error if the wrong user tries to edit a username', async () => {
|
||||
await chai.request(app)
|
||||
.post(`/api/v1/users/${mockUser.id}/username-enable`)
|
||||
.set(passport.inject({id: '456', roles: ['ADMIN']}));
|
||||
|
||||
let res = chai.request(app)
|
||||
.put('/api/v1/account/username')
|
||||
.set(passport.inject({id: 'wrongid', roles: []}))
|
||||
.send({username: 'MojoJojo'});
|
||||
|
||||
return expect(res).to.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it('it should return an error when the user tries to edit their username if canEditName is disabled', () => {
|
||||
let res = chai.request(app)
|
||||
.put('/api/v1/account/username')
|
||||
.set(passport.inject({id: mockUser.id, roles: []}))
|
||||
.send({username: 'MojoJojo'});
|
||||
|
||||
return expect(res).to.eventually.be.rejected;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -48,52 +48,3 @@ describe('/api/v1/users/:user_id/email/confirm', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/api/v1/users/:user_id/actions', () => {
|
||||
|
||||
let mockUser;
|
||||
|
||||
beforeEach(() => SettingsService.init(settings).then(() => {
|
||||
return UsersService.createLocalUser('ana@gmail.com', '123321123', 'Ana');
|
||||
})
|
||||
.then((user) => {
|
||||
mockUser = user;
|
||||
}));
|
||||
|
||||
describe('#post', () => {
|
||||
it('it should update actions', () => {
|
||||
return chai.request(app)
|
||||
.post(`/api/v1/users/${mockUser.id}/actions`)
|
||||
.set(passport.inject({id: '456', roles: ['ADMIN']}))
|
||||
.send({'action_type': 'FLAG', metadata: {reason: 'Bio is too awesome.'}})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res).to.have.body;
|
||||
expect(res.body).to.have.property('action_type', 'FLAG');
|
||||
expect(res.body).to.have.property('item_id', mockUser.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/api/v1/users/:user_id/username-enable', () => {
|
||||
let mockUser;
|
||||
|
||||
beforeEach(() => SettingsService.init(settings).then(() => {
|
||||
return UsersService.createLocalUser('ana@gmail.com', '123321123', 'Ana');
|
||||
})
|
||||
.then((user) => {
|
||||
mockUser = user;
|
||||
}));
|
||||
|
||||
describe('#post', () => {
|
||||
it('it should enable a user to edit their username', () => {
|
||||
return chai.request(app)
|
||||
.post(`/api/v1/users/${mockUser.id}/username-enable`)
|
||||
.set(passport.inject({id: '456', roles: ['ADMIN']}))
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(204);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('services.ActionsService', () => {
|
||||
expect(retrievedAction).has.property('item_id', comment.id);
|
||||
});
|
||||
|
||||
it('fires the callback sucesfully', async () => {
|
||||
it('fires the callback successfully', async () => {
|
||||
const srcAction = {
|
||||
action_type: 'LIKE',
|
||||
item_type: 'COMMENTS',
|
||||
@@ -113,7 +113,7 @@ describe('services.ActionsService', () => {
|
||||
expect(retrievedAction).is.null;
|
||||
});
|
||||
|
||||
it('fires the callback sucesfully', async () => {
|
||||
it('fires the callback successfully', async () => {
|
||||
const spy = sinon.spy();
|
||||
events.once(ACTIONS_DELETE, spy);
|
||||
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
const expect = require('chai').expect;
|
||||
const Domainlist = require('../../../services/domainlist');
|
||||
const DomainList = require('../../../services/domain_list');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
|
||||
describe('services.Domainlist', () => {
|
||||
describe('services.DomainList', () => {
|
||||
|
||||
const domainlists = {
|
||||
const domainLists = {
|
||||
whitelist: [
|
||||
'nytimes.com',
|
||||
'wapo.com'
|
||||
]
|
||||
};
|
||||
|
||||
let domainlist = new Domainlist();
|
||||
let domainList = new DomainList();
|
||||
const settings = {id: '1', moderation: 'PRE', domainlist: {whitelist: ['nytimes.com', 'wapo.com']}};
|
||||
|
||||
beforeEach(() => SettingsService.init(settings));
|
||||
|
||||
describe('#init', () => {
|
||||
|
||||
before(() => domainlist.upsert(domainlists));
|
||||
before(() => domainList.upsert(domainLists));
|
||||
|
||||
it('has entries', () => {
|
||||
expect(domainlist.lists.whitelist).to.not.be.empty;
|
||||
expect(domainList.lists.whitelist).to.not.be.empty;
|
||||
});
|
||||
|
||||
});
|
||||
@@ -92,21 +92,21 @@ describe('services.Domainlist', () => {
|
||||
['google.Ca:80', 'google.ca'],
|
||||
['google.Ca:443', 'google.ca'],
|
||||
].forEach(([domain, hostname]) => {
|
||||
expect(Domainlist.parseURL(domain), `domain ${domain} should be parsed as ${hostname}`).to.equal(hostname);
|
||||
expect(DomainList.parseURL(domain), `domain ${domain} should be parsed as ${hostname}`).to.equal(hostname);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#match', () => {
|
||||
|
||||
const whiteList = Domainlist.parseList(domainlists['whitelist']);
|
||||
const whiteList = DomainList.parseList(domainLists['whitelist']);
|
||||
|
||||
it('does match on an included domain', () => {
|
||||
[
|
||||
'http://wapo.com',
|
||||
'nytimes.com'
|
||||
].forEach((domain) => {
|
||||
expect(domainlist.match(whiteList, domain)).to.be.true;
|
||||
expect(domainList.match(whiteList, domain)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('services.Domainlist', () => {
|
||||
'www.badsite.com',
|
||||
'otherexample.com'
|
||||
].forEach((domain) => {
|
||||
expect(domainlist.match(whiteList, domain)).to.be.false;
|
||||
expect(domainList.match(whiteList, domain)).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
describe('services.scraper', () => {
|
||||
describe('#create', () => {
|
||||
it('should create a new kue job');
|
||||
});
|
||||
|
||||
describe('#scrape', () => {
|
||||
it('should scrape complete information');
|
||||
it('should scrape what it can');
|
||||
});
|
||||
|
||||
describe('#update', () => {
|
||||
it('should update the database record entries from the meta');
|
||||
});
|
||||
|
||||
describe('#process', () => {
|
||||
it('should start the processor to scrape assets');
|
||||
});
|
||||
|
||||
describe('#shutdown', () => {
|
||||
it('should shutdown the job processor');
|
||||
});
|
||||
});
|
||||
+78
-105
@@ -151,21 +151,6 @@ describe('services.UsersService', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('#setStatus', () => {
|
||||
it('should set the status to active', () => {
|
||||
return UsersService
|
||||
.setStatus(mockUsers[0].id, 'ACTIVE')
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status', 'ACTIVE');
|
||||
})
|
||||
.then(() => {
|
||||
expect(MailerService.sendSimple).to.not.have.been.called;
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ignoreUser', () => {
|
||||
it('should add user id to ignoredUsers set', async () => {
|
||||
const user = mockUsers[0];
|
||||
@@ -194,54 +179,6 @@ describe('services.UsersService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ban', () => {
|
||||
it('should set the status to banned', () => {
|
||||
return UsersService
|
||||
.setStatus(mockUsers[0].id, 'BANNED')
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status', 'BANNED');
|
||||
})
|
||||
.then(() => {
|
||||
expect(MailerService.sendSimple).to.have.been.calledWithMatch({
|
||||
template: 'banned',
|
||||
to: mockUsers[0].profiles[0].id
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should still disable and ban the user if there is no comment', () => {
|
||||
return UsersService
|
||||
.setStatus(mockUsers[0].id, 'BANNED')
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status', 'BANNED');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#unban', () => {
|
||||
it('should set the status to active', () => {
|
||||
return UsersService
|
||||
.setStatus(mockUsers[0].id, 'ACTIVE')
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status', 'ACTIVE');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#toggleNameEdit', () => {
|
||||
it('should toggle the canEditName field', () => {
|
||||
return UsersService
|
||||
.toggleNameEdit(mockUsers[0].id, true)
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('canEditName', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#search', () => {
|
||||
it('should return all the results without a value', async () => {
|
||||
expect(await UsersService.search()).to.have.length(3);
|
||||
@@ -286,53 +223,90 @@ describe('services.UsersService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#editName', () => {
|
||||
it('should let the user edit their username if the proper toggle is set', () => {
|
||||
return UsersService
|
||||
.toggleNameEdit(mockUsers[0].id, true)
|
||||
.then(() => UsersService.editName(mockUsers[0].id, 'Jojo'))
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('username', 'Jojo');
|
||||
expect(user).to.have.property('canEditName', false);
|
||||
[
|
||||
{func: 'changeUsername', okStatus: 'REJECTED', notOKStatus: 'UNSET', newStatus: 'CHANGED'},
|
||||
{func: 'setUsername', okStatus: 'UNSET', notOKStatus: 'REJECTED', newStatus: 'SET'},
|
||||
].forEach(({func, okStatus, notOKStatus, newStatus}) => {
|
||||
describe(`#${func}`, () => {
|
||||
[
|
||||
{status: okStatus},
|
||||
{error: 'EDIT_USERNAME_NOT_AUTHORIZED', status: notOKStatus},
|
||||
{error: 'EDIT_USERNAME_NOT_AUTHORIZED', status: 'SET'},
|
||||
{error: 'EDIT_USERNAME_NOT_AUTHORIZED', status: 'APPROVED'},
|
||||
{error: 'EDIT_USERNAME_NOT_AUTHORIZED', status: 'CHANGED'},
|
||||
].forEach(({status, error}) => {
|
||||
it(`${error ? 'should not' : 'should'} let them change the username if they have the status of ${status}`, async () => {
|
||||
const user = mockUsers[0];
|
||||
|
||||
// Set the user to the desired status.
|
||||
await UsersService.setUsernameStatus(user.id, status);
|
||||
|
||||
try {
|
||||
await UsersService[func](user.id, 'spock');
|
||||
} catch (err) {
|
||||
if (error) {
|
||||
expect(err).have.property('translation_key', error);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should let the user submit the same username if user is not banned (create username)', () => {
|
||||
return UsersService
|
||||
.toggleNameEdit(mockUsers[0].id, true)
|
||||
.then(() => UsersService.editName(mockUsers[0].id, mockUsers[0].username))
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('username', mockUsers[0].username);
|
||||
expect(user).to.have.property('canEditName', false);
|
||||
});
|
||||
});
|
||||
it(`should change the status to ${newStatus} when changed`, async () => {
|
||||
const user = mockUsers[0];
|
||||
|
||||
it('should return error when a banned user submits the same username (rejected username)', () => {
|
||||
return UsersService
|
||||
.toggleNameEdit(mockUsers[0].id, true)
|
||||
.then(() => UsersService.setStatus(mockUsers[0].id, 'BANNED'))
|
||||
.then(() => UsersService.editName(mockUsers[0].id, mockUsers[0].username))
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then(() => {
|
||||
throw new Error('Error expected');
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err.status).to.equal(400);
|
||||
expect(err.translation_key).to.equal('SAME_USERNAME_PROVIDED');
|
||||
});
|
||||
});
|
||||
// Set the user to the desired status.
|
||||
await UsersService.setUsernameStatus(user.id, okStatus);
|
||||
|
||||
it('should return an error if canEditName is false', async () => {
|
||||
return expect(UsersService.editName(mockUsers[0].id, 'Jojo')).to.eventually.be.rejected;
|
||||
});
|
||||
const editedUser = await UsersService[func](user.id, 'spock');
|
||||
|
||||
it('should return an error if the username is already taken', async () => {
|
||||
await UsersService.toggleNameEdit(mockUsers[0].id, true);
|
||||
return expect(UsersService.editName(mockUsers[0].id, 'Marvel')).to.eventually.be.rejected;
|
||||
});
|
||||
expect(editedUser.status.username.status).to.equal(newStatus);
|
||||
|
||||
try {
|
||||
await UsersService[func](user.id, 'spock');
|
||||
throw new Error('edit was processed successfully');
|
||||
} catch (err) {
|
||||
expect(err).have.property('translation_key', 'EDIT_USERNAME_NOT_AUTHORIZED');
|
||||
}
|
||||
});
|
||||
|
||||
it(`${func === 'changeUsername' ? 'should' : 'should not'} refuse changing the username to the same username`, async () => {
|
||||
const user = mockUsers[0];
|
||||
|
||||
// Set the user to the desired status.
|
||||
await UsersService.setUsernameStatus(user.id, okStatus);
|
||||
|
||||
if (func === 'changeUsername') {
|
||||
try {
|
||||
await UsersService[func](user.id, user.username);
|
||||
throw new Error('edit was processed successfully');
|
||||
} catch (err) {
|
||||
expect(err).have.property('translation_key', 'SAME_USERNAME_PROVIDED');
|
||||
}
|
||||
} else {
|
||||
await UsersService[func](user.id, user.username);
|
||||
}
|
||||
});
|
||||
|
||||
it('should refuse changing the username to one already taken', async () => {
|
||||
const user = mockUsers[0];
|
||||
const otherUser = mockUsers[1];
|
||||
|
||||
// Set the user to the desired status.
|
||||
await UsersService.setUsernameStatus(user.id, okStatus);
|
||||
|
||||
try {
|
||||
await UsersService[func](user.id, otherUser.username);
|
||||
throw new Error('edit was processed successfully');
|
||||
} catch (err) {
|
||||
expect(err).have.property('translation_key', 'USERNAME_IN_USE');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isValidUsername', () => {
|
||||
it('should not allow non-alphanumeric characters in usernames', () => {
|
||||
return UsersService
|
||||
.isValidUsername('hi🖕')
|
||||
@@ -344,5 +318,4 @@ describe('services.UsersService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ abab@^1.0.0, abab@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
|
||||
|
||||
abbrev@1, abbrev@1.0.x:
|
||||
abbrev@1:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
|
||||
|
||||
@@ -174,9 +174,9 @@ always-error@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/always-error/-/always-error-1.0.0.tgz#95c84042cfa86f38c86ca6c2cc42c0a0103441b2"
|
||||
|
||||
am-i-a-dependency@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/am-i-a-dependency/-/am-i-a-dependency-1.0.0.tgz#7c0e2eb126045350852e26e44f6781b3ed387919"
|
||||
am-i-a-dependency@1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/am-i-a-dependency/-/am-i-a-dependency-1.1.2.tgz#f9d3422304d6f642f821e4c407565035f6167f1f"
|
||||
|
||||
amdefine@>=0.0.4:
|
||||
version "1.0.1"
|
||||
@@ -192,10 +192,6 @@ ansi-escapes@^1.1.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
|
||||
|
||||
ansi-escapes@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b"
|
||||
|
||||
ansi-escapes@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92"
|
||||
@@ -306,6 +302,10 @@ array-equal@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
|
||||
|
||||
array-filter@~0.0.0:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
|
||||
|
||||
array-flatten@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
|
||||
@@ -317,6 +317,14 @@ array-includes@^3.0.3:
|
||||
define-properties "^1.1.2"
|
||||
es-abstract "^1.7.0"
|
||||
|
||||
array-map@~0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662"
|
||||
|
||||
array-reduce@~0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b"
|
||||
|
||||
array-union@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
|
||||
@@ -389,10 +397,6 @@ async-limiter@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
|
||||
|
||||
async@1.x, async@^1.4.0, async@^1.5.2:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
|
||||
|
||||
async@2.1.4:
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.1.4.tgz#2d2160c7788032e4dd6cbe2502f1f9a2c8f6cde4"
|
||||
@@ -405,6 +409,10 @@ async@2.4.1, async@^2.1.2, async@^2.1.4:
|
||||
dependencies:
|
||||
lodash "^4.14.0"
|
||||
|
||||
async@^1.4.0, async@^1.5.2:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
|
||||
|
||||
async@~0.9.0:
|
||||
version "0.9.2"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
|
||||
@@ -1126,7 +1134,7 @@ bluebird@3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
|
||||
|
||||
bluebird@^3.3.4, bluebird@^3.4.6, bluebird@^3.5.0, bluebird@^3.5.1:
|
||||
bluebird@3.5.1, bluebird@^3.3.4, bluebird@^3.4.6, bluebird@^3.5.0, bluebird@^3.5.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
|
||||
|
||||
@@ -1429,6 +1437,12 @@ chai-as-promised@^6.0.0:
|
||||
dependencies:
|
||||
check-error "^1.0.2"
|
||||
|
||||
chai-datetime@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/chai-datetime/-/chai-datetime-1.5.0.tgz#3742f18b024c75b76a2b7eee291662324467596c"
|
||||
dependencies:
|
||||
chai ">1.9.0"
|
||||
|
||||
chai-http@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chai-http/-/chai-http-3.0.0.tgz#5460d8036e1f1a12b0b5b5cbd529e6dc1d31eb4b"
|
||||
@@ -1446,6 +1460,17 @@ chai-nightwatch@~0.1.x:
|
||||
assertion-error "1.0.0"
|
||||
deep-eql "0.1.3"
|
||||
|
||||
chai@>1.9.0:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chai/-/chai-4.1.2.tgz#0f64584ba642f0f2ace2806279f4f06ca23ad73c"
|
||||
dependencies:
|
||||
assertion-error "^1.0.1"
|
||||
check-error "^1.0.1"
|
||||
deep-eql "^3.0.0"
|
||||
get-func-name "^2.0.0"
|
||||
pathval "^1.0.0"
|
||||
type-detect "^4.0.0"
|
||||
|
||||
chai@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247"
|
||||
@@ -1458,9 +1483,9 @@ chain-function@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc"
|
||||
|
||||
chalk@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.0.1.tgz#dbec49436d2ae15f536114e76d14656cdbc0f44d"
|
||||
chalk@2.3.0, chalk@^2.1.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
|
||||
dependencies:
|
||||
ansi-styles "^3.1.0"
|
||||
escape-string-regexp "^1.0.5"
|
||||
@@ -1476,7 +1501,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
|
||||
strip-ansi "^3.0.0"
|
||||
supports-color "^2.0.0"
|
||||
|
||||
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0:
|
||||
chalk@^2.0.0, chalk@^2.0.1:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.2.0.tgz#477b3bf2f9b8fd5ca9e429747e37f724ee7af240"
|
||||
dependencies:
|
||||
@@ -1508,7 +1533,7 @@ chdir-promise@0.4.1:
|
||||
q "1.5.0"
|
||||
spots "0.5.0"
|
||||
|
||||
check-error@^1.0.2:
|
||||
check-error@^1.0.1, check-error@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
|
||||
|
||||
@@ -2177,14 +2202,13 @@ cz-conventional-changelog@1.1.5:
|
||||
dependencies:
|
||||
word-wrap "^1.0.3"
|
||||
|
||||
cz-conventional-changelog@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cz-conventional-changelog/-/cz-conventional-changelog-2.0.0.tgz#55a979afdfe95e7024879d2a0f5924630170b533"
|
||||
cz-conventional-changelog@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cz-conventional-changelog/-/cz-conventional-changelog-2.1.0.tgz#2f4bc7390e3244e4df293e6ba351e4c740a7c764"
|
||||
dependencies:
|
||||
conventional-commit-types "^2.0.0"
|
||||
lodash.map "^4.5.1"
|
||||
longest "^1.0.1"
|
||||
pad-right "^0.2.2"
|
||||
right-pad "^1.0.1"
|
||||
word-wrap "^1.0.3"
|
||||
|
||||
@@ -2256,6 +2280,12 @@ deep-eql@0.1.3, deep-eql@^0.1.3:
|
||||
dependencies:
|
||||
type-detect "0.1.1"
|
||||
|
||||
deep-eql@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df"
|
||||
dependencies:
|
||||
type-detect "^4.0.0"
|
||||
|
||||
deep-extend@~0.4.0:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
|
||||
@@ -2587,13 +2617,13 @@ errno@^0.1.3, errno@^0.1.4:
|
||||
dependencies:
|
||||
prr "~0.0.0"
|
||||
|
||||
error-ex@^1.2.0:
|
||||
error-ex@^1.2.0, error-ex@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
|
||||
dependencies:
|
||||
is-arrayish "^0.2.1"
|
||||
|
||||
es-abstract@^1.6.1, es-abstract@^1.7.0:
|
||||
es-abstract@^1.4.3, es-abstract@^1.6.1, es-abstract@^1.7.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.9.0.tgz#690829a07cae36b222e7fd9b75c0d0573eb25227"
|
||||
dependencies:
|
||||
@@ -3271,6 +3301,10 @@ get-caller-file@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
|
||||
|
||||
get-func-name@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
|
||||
|
||||
get-stream@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
|
||||
@@ -3317,29 +3351,30 @@ ggit@1.23.1:
|
||||
ramda "0.24.1"
|
||||
semver "5.4.1"
|
||||
|
||||
ggit@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ggit/-/ggit-2.0.1.tgz#6cdfbb2bac9b63108a9609923eeca44205a9e8f4"
|
||||
ggit@2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/ggit/-/ggit-2.4.0.tgz#b99d981f3ede2a3a8a8e4bbff578bc277d400588"
|
||||
dependencies:
|
||||
always-error "1.0.0"
|
||||
bluebird "3.5.0"
|
||||
bluebird "3.5.1"
|
||||
chdir-promise "0.4.1"
|
||||
check-more-types "2.24.0"
|
||||
cli-table "0.3.1"
|
||||
colors "1.1.2"
|
||||
commander "2.11.0"
|
||||
d3-helpers "0.3.0"
|
||||
debug "2.6.8"
|
||||
debug "3.1.0"
|
||||
find-up "2.1.0"
|
||||
glob "7.1.2"
|
||||
lazy-ass "1.6.0"
|
||||
lodash "4.17.4"
|
||||
moment "2.18.1"
|
||||
moment "2.19.1"
|
||||
moment-timezone "0.5.13"
|
||||
optimist "0.6.1"
|
||||
pluralize "6.0.0"
|
||||
pluralize "7.0.0"
|
||||
q "2.0.3"
|
||||
quote "0.4.0"
|
||||
ramda "0.24.1"
|
||||
ramda "0.25.0"
|
||||
semver "5.4.1"
|
||||
|
||||
git-up@^2.0.0:
|
||||
@@ -4031,26 +4066,7 @@ inquirer@0.8.2:
|
||||
rx "^2.4.3"
|
||||
through "^2.3.6"
|
||||
|
||||
inquirer@3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.2.1.tgz#06ceb0f540f45ca548c17d6840959878265fa175"
|
||||
dependencies:
|
||||
ansi-escapes "^2.0.0"
|
||||
chalk "^2.0.0"
|
||||
cli-cursor "^2.1.0"
|
||||
cli-width "^2.0.0"
|
||||
external-editor "^2.0.4"
|
||||
figures "^2.0.0"
|
||||
lodash "^4.3.0"
|
||||
mute-stream "0.0.7"
|
||||
run-async "^2.2.0"
|
||||
rx-lite "^4.0.8"
|
||||
rx-lite-aggregates "^4.0.8"
|
||||
string-width "^2.1.0"
|
||||
strip-ansi "^4.0.0"
|
||||
through "^2.3.6"
|
||||
|
||||
inquirer@^3.0.6, inquirer@^3.2.2:
|
||||
inquirer@3.3.0, inquirer@^3.0.6, inquirer@^3.2.2:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
|
||||
dependencies:
|
||||
@@ -4412,7 +4428,7 @@ isstream@0.1.x, isstream@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
|
||||
istanbul-api@^1.1.0-alpha, istanbul-api@^1.1.1:
|
||||
istanbul-api@^1.1.1:
|
||||
version "1.1.14"
|
||||
resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.14.tgz#25bc5701f7c680c0ffff913de46e3619a3a6e680"
|
||||
dependencies:
|
||||
@@ -4475,19 +4491,6 @@ istanbul-reports@^1.1.2:
|
||||
dependencies:
|
||||
handlebars "^4.0.3"
|
||||
|
||||
istanbul@^1.1.0-alpha.1:
|
||||
version "1.1.0-alpha.1"
|
||||
resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-1.1.0-alpha.1.tgz#781795656018a2174c5f60f367ee5d361cb57b77"
|
||||
dependencies:
|
||||
abbrev "1.0.x"
|
||||
async "1.x"
|
||||
istanbul-api "^1.1.0-alpha"
|
||||
js-yaml "3.x"
|
||||
mkdirp "0.5.x"
|
||||
nopt "3.x"
|
||||
which "^1.1.1"
|
||||
wordwrap "^1.0.0"
|
||||
|
||||
items@2.x.x:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/items/-/items-2.1.1.tgz#8bd16d9c83b19529de5aea321acaada78364a198"
|
||||
@@ -4759,7 +4762,7 @@ js-yaml@0.3.x:
|
||||
version "0.3.7"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-0.3.7.tgz#d739d8ee86461e54b354d6a7d7d1f2ad9a167f62"
|
||||
|
||||
js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.5.2, js-yaml@^3.7.0, js-yaml@^3.9.1:
|
||||
js-yaml@^3.4.3, js-yaml@^3.5.2, js-yaml@^3.7.0, js-yaml@^3.9.1:
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc"
|
||||
dependencies:
|
||||
@@ -4854,6 +4857,10 @@ json-loader@^0.5.4, json-loader@^0.5.7:
|
||||
version "0.5.7"
|
||||
resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
|
||||
|
||||
json-parse-better-errors@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz#50183cd1b2d25275de069e9e71b467ac9eab973a"
|
||||
|
||||
json-schema-traverse@^0.3.0:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
|
||||
@@ -5105,6 +5112,15 @@ load-json-file@^2.0.0:
|
||||
pify "^2.0.0"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
load-json-file@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
|
||||
dependencies:
|
||||
graceful-fs "^4.1.2"
|
||||
parse-json "^4.0.0"
|
||||
pify "^3.0.0"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
loader-runner@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
|
||||
@@ -5541,6 +5557,10 @@ memory-fs@^0.4.0, memory-fs@~0.4.1:
|
||||
errno "^0.1.3"
|
||||
readable-stream "^2.0.1"
|
||||
|
||||
memorystream@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
|
||||
|
||||
merge-descriptors@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
|
||||
@@ -5697,11 +5717,17 @@ mocha@^3.1.2:
|
||||
mkdirp "0.5.1"
|
||||
supports-color "3.1.2"
|
||||
|
||||
moment-timezone@0.5.13:
|
||||
version "0.5.13"
|
||||
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.13.tgz#99ce5c7d827262eb0f1f702044177f60745d7b90"
|
||||
dependencies:
|
||||
moment ">= 2.9.0"
|
||||
|
||||
moment@2.18.1:
|
||||
version "2.18.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
|
||||
|
||||
moment@2.x.x, moment@^2.10.3, moment@^2.18.1:
|
||||
moment@2.19.1, moment@2.x.x, "moment@>= 2.9.0", moment@^2.10.3, moment@^2.18.1:
|
||||
version "2.19.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.1.tgz#56da1a2d1cbf01d38b7e1afc31c10bcfa1929167"
|
||||
|
||||
@@ -6042,12 +6068,6 @@ nomnom@~1.6.2:
|
||||
colors "0.5.x"
|
||||
underscore "~1.4.4"
|
||||
|
||||
nopt@3.x:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
|
||||
dependencies:
|
||||
abbrev "1"
|
||||
|
||||
nopt@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
|
||||
@@ -6089,6 +6109,20 @@ normalize-url@^1.4.0:
|
||||
query-string "^4.1.0"
|
||||
sort-keys "^1.0.0"
|
||||
|
||||
npm-run-all@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.2.tgz#90d62d078792d20669139e718621186656cea056"
|
||||
dependencies:
|
||||
ansi-styles "^3.2.0"
|
||||
chalk "^2.1.0"
|
||||
cross-spawn "^5.1.0"
|
||||
memorystream "^0.3.1"
|
||||
minimatch "^3.0.4"
|
||||
ps-tree "^1.1.0"
|
||||
read-pkg "^3.0.0"
|
||||
shell-quote "^1.6.1"
|
||||
string.prototype.padend "^3.0.0"
|
||||
|
||||
npm-run-path@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
||||
@@ -6307,12 +6341,6 @@ package-json@^4.0.0:
|
||||
registry-url "^3.0.3"
|
||||
semver "^5.1.0"
|
||||
|
||||
pad-right@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/pad-right/-/pad-right-0.2.2.tgz#6fbc924045d244f2a2a244503060d3bfc6009774"
|
||||
dependencies:
|
||||
repeat-string "^1.5.2"
|
||||
|
||||
pako@~0.2.0:
|
||||
version "0.2.9"
|
||||
resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
|
||||
@@ -6342,6 +6370,13 @@ parse-json@^2.2.0:
|
||||
dependencies:
|
||||
error-ex "^1.2.0"
|
||||
|
||||
parse-json@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
|
||||
dependencies:
|
||||
error-ex "^1.3.1"
|
||||
json-parse-better-errors "^1.0.1"
|
||||
|
||||
parse-url@^1.3.0:
|
||||
version "1.3.11"
|
||||
resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-1.3.11.tgz#57c15428ab8a892b1f43869645c711d0e144b554"
|
||||
@@ -6441,6 +6476,16 @@ path-type@^2.0.0:
|
||||
dependencies:
|
||||
pify "^2.0.0"
|
||||
|
||||
path-type@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
|
||||
dependencies:
|
||||
pify "^3.0.0"
|
||||
|
||||
pathval@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
|
||||
|
||||
pause-stream@0.0.11:
|
||||
version "0.0.11"
|
||||
resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
|
||||
@@ -6515,7 +6560,7 @@ pluralize@6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-6.0.0.tgz#d9b51afad97d3d51075cc1ddba9b132cacccb7ba"
|
||||
|
||||
pluralize@^7.0.0:
|
||||
pluralize@7.0.0, pluralize@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
|
||||
|
||||
@@ -6946,22 +6991,22 @@ postcss@^6.0.1:
|
||||
source-map "^0.6.1"
|
||||
supports-color "^4.4.0"
|
||||
|
||||
pre-git@^3.15.3:
|
||||
version "3.15.3"
|
||||
resolved "https://registry.yarnpkg.com/pre-git/-/pre-git-3.15.3.tgz#834a598a0608b821ba1e9e217f58d510bc7b0ca9"
|
||||
pre-git@^3.16.0:
|
||||
version "3.16.0"
|
||||
resolved "https://registry.yarnpkg.com/pre-git/-/pre-git-3.16.0.tgz#a7656bc5f277185fd213c78f39f24f2cb603eb61"
|
||||
dependencies:
|
||||
bluebird "3.5.0"
|
||||
chalk "2.0.1"
|
||||
bluebird "3.5.1"
|
||||
chalk "2.3.0"
|
||||
check-more-types "2.24.0"
|
||||
conventional-commit-message "1.1.0"
|
||||
cz-conventional-changelog "2.0.0"
|
||||
debug "2.6.8"
|
||||
ggit "2.0.1"
|
||||
inquirer "3.2.1"
|
||||
cz-conventional-changelog "2.1.0"
|
||||
debug "2.6.9"
|
||||
ggit "2.4.0"
|
||||
inquirer "3.3.0"
|
||||
lazy-ass "1.6.0"
|
||||
require-relative "0.8.7"
|
||||
shelljs "0.7.8"
|
||||
simple-commit-message "3.3.1"
|
||||
simple-commit-message "3.3.2"
|
||||
validate-commit-msg "2.14.0"
|
||||
word-wrap "1.2.3"
|
||||
|
||||
@@ -7271,6 +7316,10 @@ ramda@0.24.1, ramda@^0.24.1:
|
||||
version "0.24.1"
|
||||
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.24.1.tgz#c3b7755197f35b8dc3502228262c4c91ddb6b857"
|
||||
|
||||
ramda@0.25.0:
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.25.0.tgz#8fdf68231cffa90bc2f9460390a0cb74a29b29a9"
|
||||
|
||||
randexp@^0.4.2:
|
||||
version "0.4.6"
|
||||
resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
|
||||
@@ -7485,6 +7534,14 @@ read-pkg@^2.0.0:
|
||||
normalize-package-data "^2.3.2"
|
||||
path-type "^2.0.0"
|
||||
|
||||
read-pkg@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
|
||||
dependencies:
|
||||
load-json-file "^4.0.0"
|
||||
normalize-package-data "^2.3.2"
|
||||
path-type "^3.0.0"
|
||||
|
||||
readable-stream@1.1, readable-stream@1.1.x:
|
||||
version "1.1.13"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e"
|
||||
@@ -8053,6 +8110,15 @@ shebang-regex@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
|
||||
|
||||
shell-quote@^1.6.1:
|
||||
version "1.6.1"
|
||||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767"
|
||||
dependencies:
|
||||
array-filter "~0.0.0"
|
||||
array-map "~0.0.0"
|
||||
array-reduce "~0.0.0"
|
||||
jsonify "~0.0.0"
|
||||
|
||||
shelljs@0.3.x:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.3.0.tgz#3596e6307a781544f591f37da618360f31db57b1"
|
||||
@@ -8073,13 +8139,13 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
|
||||
|
||||
simple-commit-message@3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/simple-commit-message/-/simple-commit-message-3.3.1.tgz#b69026a9692e0f470233a4c5c1df3432cdb50560"
|
||||
simple-commit-message@3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/simple-commit-message/-/simple-commit-message-3.3.2.tgz#52bdadb7f4f680d8b29c07af1826a6611f7ee783"
|
||||
dependencies:
|
||||
am-i-a-dependency "1.0.0"
|
||||
am-i-a-dependency "1.1.2"
|
||||
check-more-types "2.24.0"
|
||||
debug "2.6.8"
|
||||
debug "2.6.9"
|
||||
ggit "1.23.1"
|
||||
hr "0.1.3"
|
||||
inquirer "0.12.0"
|
||||
@@ -8335,6 +8401,14 @@ string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
|
||||
is-fullwidth-code-point "^2.0.0"
|
||||
strip-ansi "^4.0.0"
|
||||
|
||||
string.prototype.padend@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0"
|
||||
dependencies:
|
||||
define-properties "^1.1.2"
|
||||
es-abstract "^1.4.3"
|
||||
function-bind "^1.0.2"
|
||||
|
||||
string_decoder@^0.10.25, string_decoder@~0.10.x:
|
||||
version "0.10.31"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
|
||||
@@ -9079,7 +9153,7 @@ which-module@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
||||
|
||||
which@^1.1.1, which@^1.2.12, which@^1.2.9:
|
||||
which@^1.2.12, which@^1.2.9:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
|
||||
dependencies:
|
||||
@@ -9139,14 +9213,14 @@ wordwrap@0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
|
||||
|
||||
wordwrap@^1.0.0, wordwrap@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||
|
||||
wordwrap@~0.0.2:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
|
||||
|
||||
wordwrap@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||
|
||||
worker-farm@^1.3.1:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.0.tgz#adfdf0cd40581465ed0a1f648f9735722afd5c8d"
|
||||
|
||||
Reference in New Issue
Block a user