Merge branch 'master' into docs-update

This commit is contained in:
Wyatt Johnson
2017-07-27 11:21:40 +10:00
committed by GitHub
19 changed files with 326 additions and 130 deletions
+2 -2
View File
@@ -90,7 +90,7 @@ async function onListening() {
let bind = typeof addr === 'string'
? `pipe ${addr}`
: `port ${addr.port}`;
console.log(`API Server Listening on ${bind}`);
debug(`API Server Listening on ${bind}`);
}
/**
@@ -149,7 +149,7 @@ async function startApp(program) {
// Mount the websocket server if requested.
if (program.websockets) {
console.log(`Websocket Server Listening on ${port}`);
debug(`Websocket Server Listening on ${port}`);
// Mount the subscriptions server on the application server.
createSubscriptionManager(server);
+16 -1
View File
@@ -14,7 +14,7 @@ require('env-rewrite').rewrite();
const CONFIG = {
// WEBPACK indicates when webpack is currently building.
WEBPACK: process.env.WEBPACK === 'true',
WEBPACK: process.env.WEBPACK === 'TRUE',
//------------------------------------------------------------------------------
// JWT based configuration
@@ -24,6 +24,17 @@ const CONFIG = {
// application.
JWT_SECRET: process.env.TALK_JWT_SECRET || null,
// JWT_SECRETS is used when key rotation is available.
JWT_SECRETS: process.env.TALK_JWT_SECRETS || null,
// JWT_COOKIE_NAME is the name of the cookie optionally containing the JWT
// token.
JWT_COOKIE_NAME: process.env.TALK_JWT_COOKIE_NAME || 'authorization',
// JWT_CLEAR_COOKIE_LOGOUT specifies whether the named cookie should be
// cleared when the user is logged out.
JWT_CLEAR_COOKIE_LOGOUT: process.env.TALK_JWT_CLEAR_COOKIE_LOGOUT ? process.env.TALK_JWT_CLEAR_COOKIE_LOGOUT !== 'FALSE' : true,
// JWT_AUDIENCE is the value for the audience claim for the tokens that will be
// verified when decoding. If `JWT_AUDIENCE` is not in the environment, then it
// will default to `talk`.
@@ -102,6 +113,10 @@ const CONFIG = {
// JWT based configuration
//------------------------------------------------------------------------------
if (CONFIG.JWT_SECRETS) {
CONFIG.JWT_SECRETS = JSON.parse(CONFIG.JWT_SECRETS);
}
if (process.env.NODE_ENV === 'test' && !CONFIG.JWT_SECRET) {
CONFIG.JWT_SECRET = 'keyboard cat';
} else if (!CONFIG.JWT_SECRET) {
+4 -4
View File
@@ -6,11 +6,11 @@
"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\"",
"dev-start": "nodemon -w . -w bin/cli -w bin/cli-serve --config .nodemon.json --exec \"WEBPACK=TRUE NODE_ENV=test ./scripts/generateIntrospectionResult.js && ./bin/cli -c .env serve -j -w\"",
"prebuild": "yarn generate-introspection",
"build": "WEBPACK=true NODE_ENV=production webpack -p --config webpack.config.js --bail",
"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",
"build-watch": "WEBPACK=TRUE NODE_ENV=development webpack --progress --config webpack.config.js --watch",
"lint": "eslint bin/* .",
"lint-fix": "eslint bin/* . --fix",
"test": "TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
@@ -20,7 +20,7 @@
"poste2e": "NODE_ENV=test scripts/poste2e.sh",
"embed-start": "NODE_ENV=development yarn build && ./bin/cli serve --jobs",
"heroku-postbuild": "./bin/cli plugins reconcile && yarn build",
"generate-introspection": "WEBPACK=true NODE_ENV=test ./scripts/generateIntrospectionResult.js"
"generate-introspection": "WEBPACK=TRUE NODE_ENV=test ./scripts/generateIntrospectionResult.js"
},
"talk": {
"migration": {
@@ -54,6 +54,13 @@ export default (reaction) => (WrappedComponent) => {
id: fragmentId
});
if (!data) {
if (self) {
throw new Error(`Comment ${action.item_id} was not found`);
}
return;
}
// Add our comment from the mutation to the end.
let idx = data.action_summaries.findIndex(isReaction);
@@ -96,6 +103,13 @@ export default (reaction) => (WrappedComponent) => {
id: fragmentId
});
if (!data) {
if (self) {
throw new Error(`Comment ${action.item_id} was not found`);
}
return;
}
// Check whether we liked this comment.
const idx = data.action_summaries.findIndex(isReaction);
@@ -8,10 +8,10 @@
}
.tag {
background: #3D73D5;
background: #D2D7D3;
font-size: 12px;
font-weight: bold;
color: white;
color: #4A4A4A;
display: inline-block;
margin: 0px 5px;
padding: 5px 5px;
@@ -1,9 +1,9 @@
import {VIEWING_OPTIONS_OPEN, VIEWING_OPTIONS_CLOSE} from './constants';
import {OPEN_MENU, CLOSE_MENU} from './constants';
export const openViewingOptions = () => ({
type: VIEWING_OPTIONS_OPEN
export const openMenu = () => ({
type: OPEN_MENU,
});
export const closeViewingOptions = () => ({
type: VIEWING_OPTIONS_CLOSE
export const closeMenu = () => ({
type: CLOSE_MENU,
});
@@ -8,15 +8,15 @@ import {Slot, ClickOutside} from 'plugin-api/beta/client/components';
const ViewingOptions = (props) => {
const toggleOpen = () => {
if (!props.open) {
props.openViewingOptions();
props.openMenu();
} else {
props.closeViewingOptions();
props.closeMenu();
}
};
const handleClickOutside = () => {
if (props.open) {
props.closeViewingOptions();
props.closeMenu();
}
};
@@ -1,2 +1,5 @@
export const VIEWING_OPTIONS_OPEN = 'VIEWING_OPTIONS_OPEN';
export const VIEWING_OPTIONS_CLOSE = 'VIEWING_OPTIONS_CLOSE';
const prefix = 'TALK_VIEWING_OPTIONS';
export const OPEN_MENU = `${prefix}_OPEN_MENU`;
export const CLOSE_MENU = `${prefix}_CLOSE_MENU`;
@@ -1,13 +1,13 @@
import {connect} from 'plugin-api/beta/client/hocs';
import {bindActionCreators} from 'redux';
import ViewingOptions from '../components/ViewingOptions';
import {openViewingOptions, closeViewingOptions} from '../actions';
import {openMenu, closeMenu} from '../actions';
const mapStateToProps = ({coralPluginViewingOptions: state}) => ({
open: state.open
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({openViewingOptions, closeViewingOptions}, dispatch);
bindActionCreators({openMenu, closeMenu}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(ViewingOptions);
@@ -1,17 +1,17 @@
import {VIEWING_OPTIONS_OPEN, VIEWING_OPTIONS_CLOSE} from './constants';
import {OPEN_MENU, CLOSE_MENU} from './constants';
const initialState = {
open: false
};
export default function offTopic (state = initialState, action) {
export default function reducer(state = initialState, action) {
switch (action.type) {
case VIEWING_OPTIONS_OPEN:
case OPEN_MENU:
return {
...state,
open: true
};
case VIEWING_OPTIONS_CLOSE:
case CLOSE_MENU:
return {
...state,
open: false
@@ -1,2 +1,4 @@
export const SHOW_TOOLTIP = 'SHOW_TOOLTIP';
export const HIDE_TOOLTIP = 'HIDE_TOOLTIP';
const prefix = 'TALK_FEATURED_COMMENTS';
export const SHOW_TOOLTIP = `${prefix}_SHOW_TOOLTIP`;
export const HIDE_TOOLTIP = `${prefix}_HIDE_TOOLTIP`;
+48
View File
@@ -0,0 +1,48 @@
const {
JWT_SECRETS,
JWT_SECRET,
JWT_ALG
} = require('./config');
const debug = require('debug')('talk:secrets');
const jwt = require('./services/jwt');
if (JWT_SECRETS) {
if (!Array.isArray(JWT_SECRETS)) {
throw new Error('TALK_JWT_SECRETS must be a JSON array in the form [{"kid": kid, ["secret": secret | "private": private, "public": public]}, ...]');
}
if (JWT_SECRETS.length === 0) {
throw new Error('TALK_JWT_SECRETS must be a JSON array with non zero length');
}
// Wrap a multi-secret around the available secrets.
module.exports.jwt = new jwt.MultiSecret(JWT_SECRETS.map((secret) => {
if (!('kid' in secret)) {
throw new Error('when multiple keys are specified, kid\'s must be specified');
}
// HMAC secrets do not have public/private keys.
if (JWT_ALG.startsWith('HS')) {
return new jwt.SharedSecret(secret, JWT_ALG);
}
if (!('public' in secret)) {
throw new Error('all symetric keys must provide a PEM encoded public key');
}
return new jwt.AsymmetricSecret(secret, JWT_ALG);
}));
debug(`loaded ${JWT_SECRET.length} secrets`);
} else if (JWT_SECRET) {
if (JWT_ALG.startsWith('HS')) {
module.exports.jwt = new jwt.SharedSecret({
secret: JWT_SECRET
}, JWT_ALG);
} else {
module.exports.jwt = new jwt.AsymmetricSecret(JSON.parse(JWT_SECRET), JWT_ALG);
}
debug('loaded 1 secret');
}
+118
View File
@@ -0,0 +1,118 @@
const jwt = require('jsonwebtoken');
/**
* MultiSecret will take many secrets and provide a unified interface for
* handling verifying and signing.
*/
class MultiSecret {
constructor(secrets) {
this.secrets = secrets;
}
/**
* Sign will sign with the first secret.
*/
sign(payload, options) {
return this.secrets[0].sign(payload, options);
}
/**
* Verify will parse the token and determine the kid, then match it to the
* available secrets, using that to perform the verification.
*/
verify(token, options, callback) {
let header = null;
try {
header = JSON.parse(Buffer(token.split('.')[0], 'base64').toString());
} catch(err) {
return callback(err);
}
if (!('kid' in header)) {
return callback(new Error('expected kid to exist in the token header, it did not.'));
}
let kid = header.kid;
let verifier = this.secrets.find((secret) => secret.kid === kid);
if (!verifier) {
return callback(new Error(`expected kid ${kid} was not available.`));
}
return verifier.verify(token, options, callback);
}
}
/**
* Secret wraps the capabilities expected of a Secret, signing and verifying.
*/
class Secret {
constructor({kid, signingKey, verifiyingKey, algorithm}) {
this.kid = kid;
this.signingKey = signingKey;
this.verifiyingKey = verifiyingKey;
this.algorithm = algorithm;
}
/**
* Sign will sign the payload with the secret.
*
* @param {Object} payload the object to sign
* @param {Object} options the signing options
*/
sign(payload, options) {
if (!this.signingKey) {
throw new Error('no signing key on secret, cannot sign');
}
return jwt.sign(payload, this.signingKey, Object.assign({}, options, {
keyid: this.kid,
algorithm: this.algorithm
}));
}
/**
* Verify will ensure that the given token was indeed signed with this secret.
* @param {String} token the token to verify
* @param {Object} options the verification options
* @param {Function} callback the function to call with the verification results
*/
verify(token, options, callback) {
jwt.verify(token, this.verifiyingKey, Object.assign({}, options, {
algorithms: [this.algorithm]
}), callback);
}
}
/**
* SharedSecret is the HMAC based secret that's used for signing/verifying.
*/
function SharedSecret({kid = undefined, secret}, algorithm) {
return new Secret({
kid,
signingKey: secret,
verifiyingKey: secret,
algorithm
});
}
/**
* AsymmetricSecret is the Asymmetric based key, where a private key is optional
* and the public key is required.
*/
function AsymmetricSecret({kid = undefined, private: privateKey, public: publicKey}, algorithm) {
publicKey = Buffer.from(publicKey.replace(/\\n/g, '\n'));
privateKey = privateKey ? Buffer.from(privateKey.replace(/\\n/g, '\n')) : null;
return new Secret({
kid,
signingKey: privateKey,
verifiyingKey: publicKey,
algorithm
});
}
module.exports = {
AsymmetricSecret,
SharedSecret,
MultiSecret
};
+1 -1
View File
@@ -43,7 +43,7 @@ if (enabled('talk:db')) {
if (WEBPACK) {
console.warn('Not connecting to mongodb during webpack build');
debug('Not connecting to mongodb during webpack build');
// @wyattjoh: We didn't call connect, but because we include mongoose, it will hold the socket ready,
// preventing node from exiting. Calling disconnect here just ensures that the application
+25 -15
View File
@@ -4,7 +4,6 @@ const SettingsService = require('./settings');
const TokensService = require('./tokens');
const fetch = require('node-fetch');
const FormData = require('form-data');
const JWT = require('jsonwebtoken');
const LocalStrategy = require('passport-local').Strategy;
const errors = require('../errors');
const uuid = require('uuid');
@@ -17,30 +16,38 @@ const {createClientFactory} = require('./redis');
const client = createClientFactory();
const {
JWT_SECRET,
JWT_ISSUER,
JWT_EXPIRY,
JWT_AUDIENCE,
JWT_ALG,
RECAPTCHA_SECRET,
RECAPTCHA_ENABLED
RECAPTCHA_ENABLED,
JWT_COOKIE_NAME,
JWT_CLEAR_COOKIE_LOGOUT
} = require('../config');
const {
jwt: JWT_SECRET
} = require('../secrets');
// GenerateToken will sign a token to include all the authorization information
// needed for the front end.
const GenerateToken = (user) => JWT.sign({}, JWT_SECRET, {
jwtid: uuid.v4(),
expiresIn: JWT_EXPIRY,
issuer: JWT_ISSUER,
subject: user.id,
audience: JWT_AUDIENCE
});
const GenerateToken = (user) => {
return JWT_SECRET.sign({}, {
jwtid: uuid.v4(),
expiresIn: JWT_EXPIRY,
issuer: JWT_ISSUER,
subject: user.id,
audience: JWT_AUDIENCE,
algorithm: JWT_ALG
});
};
// SetTokenForSafari sends the token in a cookie for Safari clients.
const SetTokenForSafari = (req, res, token) => {
const browser = bowser._detect(req.headers['user-agent']);
if (browser.ios || browser.safari) {
res.cookie('authorization', token, {
res.cookie(JWT_COOKIE_NAME, token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
expires: new Date(Date.now() + ms(JWT_EXPIRY))
@@ -154,7 +161,11 @@ const HandleLogout = (req, res, next) => {
return next(err);
}
res.clearCookie('authorization');
// Only clear the cookie on logout if enabled.
if (JWT_CLEAR_COOKIE_LOGOUT) {
res.clearCookie(JWT_COOKIE_NAME);
}
res.status(204).end();
});
};
@@ -187,7 +198,6 @@ const CheckBlacklisted = async (jwt) => {
return checkGeneralTokenBlacklist(jwt);
};
const jwt = require('jsonwebtoken');
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
@@ -195,7 +205,7 @@ let cookieExtractor = function(req) {
let token = null;
if (req && req.cookies) {
token = req.cookies['authorization'];
token = req.cookies[JWT_COOKIE_NAME];
}
return token;
@@ -204,7 +214,7 @@ let cookieExtractor = function(req) {
// Override the JwtVerifier method on the JwtStrategy so we can pack the
// original token into the payload.
JwtStrategy.JwtVerifier = (token, secretOrKey, options, callback) => {
return jwt.verify(token, secretOrKey, options, (err, jwt) => {
return JWT_SECRET.verify(token, options, (err, jwt) => {
if (err) {
return callback(err);
}
+5 -3
View File
@@ -1,14 +1,16 @@
const errors = require('../errors');
const UserModel = require('../models/user');
const JWT = require('jsonwebtoken');
const uuid = require('uuid');
const {
JWT_SECRET,
JWT_ISSUER,
JWT_AUDIENCE
} = require('../config');
const {
jwt: JWT_SECRET
} = require('../secrets');
/**
* TokenService manages Personal Access Tokens for users. These tokens are
* persisted in the database and attached to the user.
@@ -33,7 +35,7 @@ module.exports = class TokenService {
};
// Sign the payload.
const jwt = JWT.sign(payload, JWT_SECRET, {});
const jwt = JWT_SECRET.sign(payload, {});
// Create the PAT.
let pat = {
+66 -82
View File
@@ -2,13 +2,17 @@ const assert = require('assert');
const uuid = require('uuid');
const bcrypt = require('bcryptjs');
const url = require('url');
const jwt = require('jsonwebtoken');
const Wordlist = require('./wordlist');
const errors = require('../errors');
const {
JWT_SECRET,
ROOT_URL
} = require('../config');
const {
jwt: JWT_SECRET
} = require('../secrets');
const debug = require('debug')('talk:services:users');
const UserModel = require('../models/user');
@@ -354,7 +358,7 @@ module.exports = class UsersService {
*/
static disableUser(id) {
return UserModel.update({
id: id
id
}, {
$set: {
disabled: true
@@ -369,7 +373,7 @@ module.exports = class UsersService {
*/
static enableUser(id) {
return UserModel.update({
id: id
id
}, {
$set: {
disabled: false
@@ -413,9 +417,7 @@ module.exports = class UsersService {
return Promise.reject(new Error(`role ${role} is not supported`));
}
return UserModel.update({
id: id
}, {
return UserModel.update({id}, {
$pull: {
roles: role
}
@@ -461,16 +463,15 @@ module.exports = class UsersService {
* @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,
});
const user = await UserModel.findOneAndUpdate({id}, {
$set: {
suspension: {
until,
},
}
}, {
new: true,
});
if (message) {
let localProfile = user.profiles.find((profile) => profile.provider === 'local');
@@ -500,9 +501,7 @@ module.exports = class UsersService {
* @param {Date} until date until the suspension is valid.
*/
static async rejectUsername(id, message) {
const user = await UserModel.findOneAndUpdate({
id
}, {
const user = await UserModel.findOneAndUpdate({id}, {
$set: {
status: 'BANNED',
canEditName: true,
@@ -512,18 +511,17 @@ module.exports = class UsersService {
});
if (message) {
let localProfile = user.profiles.find((profile) => profile.provider === 'local');
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
};
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);
}
@@ -548,9 +546,7 @@ module.exports = class UsersService {
static async findOrCreateByIDToken(id, token) {
// Try to get the user.
let user = await UserModel.findOne({
id
});
let user = await UserModel.findOne({id});
// If the user was not found, try to look it up.
if (user === null) {
@@ -629,8 +625,7 @@ module.exports = class UsersService {
version: user.__v
};
return jwt.sign(payload, JWT_SECRET, {
algorithm: 'HS256',
return JWT_SECRET.sign(payload, {
expiresIn: '1d',
subject: PASSWORD_RESET_JWT_SUBJECT
});
@@ -644,11 +639,7 @@ module.exports = class UsersService {
*/
static verifyToken(token, options = {}) {
return new Promise((resolve, reject) => {
// Set the allowed algorithms.
options.algorithms = ['HS256'];
jwt.verify(token, JWT_SECRET, options, (err, decoded) => {
JWT_SECRET.verify(token, options, (err, decoded) => {
if (err) {
return reject(err);
}
@@ -762,7 +753,7 @@ module.exports = class UsersService {
* @param {String} email The email that we are needing to get confirmed.
* @return {Promise}
*/
static createEmailConfirmToken(userID = null, email, referer = ROOT_URL) {
static async createEmailConfirmToken(userID = null, email, referer = ROOT_URL) {
if (!email || typeof email !== 'string') {
return Promise.reject('email is required when creating a JWT for resetting passord');
}
@@ -772,41 +763,37 @@ module.exports = class UsersService {
const tokenOptions = {
jwtid: uuid.v4(),
algorithm: 'HS256',
expiresIn: '1d',
subject: EMAIL_CONFIRM_JWT_SUBJECT
};
let userPromise;
let user;
if (!userID) {
// If there is no userID, we're coming from the endpoint where a new user
// is re-requesting a confirmation email and we don't know the userID.
userPromise = UserModel.findOne({profiles: {$elemMatch: {id: email, provider: 'local'}}});
user = await UserModel.findOne({profiles: {$elemMatch: {id: email, provider: 'local'}}});
} else {
userPromise = UsersService.findById(userID);
user = await UsersService.findById(userID);
}
return userPromise.then((user) => {
if (!user) {
return Promise.reject(errors.ErrNotFound);
}
if (!user) {
throw errors.ErrNotFound;
}
// Get the profile representing the local account.
let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local');
// Get the profile representing the local account.
let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local');
// Ensure that the user email hasn't already been verified.
if (profile && profile.metadata && profile.metadata.confirmed_at) {
return Promise.reject(new Error('email address already confirmed'));
}
// Ensure that the user email hasn't already been verified.
if (profile && profile.metadata && profile.metadata.confirmed_at) {
throw new Error('email address already confirmed');
}
return jwt.sign({
email,
referer,
userID: user.id
}, JWT_SECRET, tokenOptions);
});
return JWT_SECRET.sign({
email,
referer,
userID: user.id
}, tokenOptions);
}
/**
@@ -816,17 +803,14 @@ module.exports = class UsersService {
* signed with our secret.
* @return {Promise}
*/
static verifyEmailConfirmation(token) {
return UsersService
.verifyToken(token, {
subject: EMAIL_CONFIRM_JWT_SUBJECT
})
.then(({userID, email, referer}) => {
return UsersService
.confirmEmail(userID, email)
.then(() => ({userID, email, referer}));
});
static async verifyEmailConfirmation(token) {
let {userID, email, referer} = await UsersService.verifyToken(token, {
subject: EMAIL_CONFIRM_JWT_SUBJECT
});
await UsersService.confirmEmail(userID, email);
return {userID, email, referer};
}
/**
@@ -835,7 +819,7 @@ module.exports = class UsersService {
static confirmEmail(id, email) {
return UserModel
.update({
id: id,
id,
profiles: {
$elemMatch: {
id: email,
@@ -934,18 +918,18 @@ module.exports = class UsersService {
/**
* Ignore another user
* @param {String} userId the id of the user that is ignoring another users
* @param {String[]} usersToIgnore Array of user IDs to ignore
* @param {String} id the id of the user that is ignoring another users
* @param {Array<String>} usersToIgnore Array of user IDs to ignore
*/
static ignoreUsers(userId, usersToIgnore) {
static 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(userId)) {
if (usersToIgnore.includes(id)) {
throw new Error('Users cannot ignore themselves');
}
// TODO: For each usersToIgnore, make sure they exist?
return UserModel.update({id: userId}, {
return UserModel.update({id}, {
$addToSet: {
ignoresUsers: {
$each: usersToIgnore
@@ -957,12 +941,12 @@ module.exports = class UsersService {
/**
* Stop ignoring other users
* @param {String} userId the id of the user that is ignoring another users
* @param {String[]} usersToStopIgnoring Array of user IDs to stop ignoring
* @param {Array<String>} usersToStopIgnoring Array of user IDs to stop ignoring
*/
static async stopIgnoringUsers(userId, usersToStopIgnoring) {
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: userId}, {
await UserModel.update({id}, {
$pullAll: {
ignoresUsers: usersToStopIgnoring
}
-1
View File
@@ -37,7 +37,6 @@ describe('graph.mutations.addTag', () => {
console.error(res.errors);
}
console.log('res.errors', res.errors);
expect(res.errors).to.be.empty;
let {tags} = await CommentsService.findById(comment.id);
+2 -1
View File
@@ -7,6 +7,7 @@ const _ = require('lodash');
const Copy = require('copy-webpack-plugin');
const LicenseWebpackPlugin = require('license-webpack-plugin');
const webpack = require('webpack');
const debug = require('debug')('talk:webpack');
// Possibly load the config from the .env file (if there is one).
require('dotenv').config();
@@ -15,7 +16,7 @@ const {plugins, pluginsPath, PluginManager} = require('./plugins');
const manager = new PluginManager(plugins);
const targetPlugins = manager.section('targets').plugins;
console.log(`Using ${pluginsPath} as the plugin configuration path`);
debug(`Using ${pluginsPath} as the plugin configuration path`);
const buildTargets = [
'coral-admin',