diff --git a/.gitignore b/.gitignore
index f47efe570..70b6d482e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ client/coral-framework/graphql/introspection.json
.idea/
*.swp
*.DS_STORE
+.prettierrc.json
coverage/
test/e2e/reports/
diff --git a/app.js b/app.js
index 597da28b9..5826a0272 100644
--- a/app.js
+++ b/app.js
@@ -1,8 +1,10 @@
const express = require('express');
const morgan = require('morgan');
const path = require('path');
+const uuid = require('uuid');
const merge = require('lodash/merge');
const helmet = require('helmet');
+const plugins = require('./services/plugins');
const compression = require('compression');
const {HELMET_CONFIGURATION} = require('./config');
const {MOUNT_PATH} = require('./url');
@@ -12,6 +14,25 @@ const {ENABLE_TRACING, APOLLO_ENGINE_KEY, PORT} = require('./config');
const app = express();
+// Request Identity Middleware
+app.use((req, res, next) => {
+ req.id = uuid.v4();
+
+ next();
+});
+
+//==============================================================================
+// PLUGIN PRE APPLICATION MIDDLEWARE
+//==============================================================================
+
+// Inject server route plugins.
+plugins.get('server', 'app').forEach(({plugin, app: callback}) => {
+ debug(`added plugin '${plugin.name}'`);
+
+ // Pass the app to the plugin to mount it's routes.
+ callback(app);
+});
+
//==============================================================================
// APPLICATION WIDE MIDDLEWARE
//==============================================================================
diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js
index a3ff2ca3e..18e31091f 100644
--- a/client/coral-admin/src/actions/moderation.js
+++ b/client/coral-admin/src/actions/moderation.js
@@ -34,3 +34,8 @@ export const storySearchChange = (value) => ({
export const clearState = () => ({
type: actions.MODERATION_CLEAR_STATE
});
+
+export const selectCommentId = (id) => ({
+ type: actions.MODERATION_SELECT_COMMENT,
+ id,
+});
diff --git a/client/coral-admin/src/components/CommentDetails.js b/client/coral-admin/src/components/CommentDetails.js
index cd504a40f..418be0354 100644
--- a/client/coral-admin/src/components/CommentDetails.js
+++ b/client/coral-admin/src/components/CommentDetails.js
@@ -21,10 +21,11 @@ class CommentDetails extends Component {
this.setState((state) => ({
showDetail: !state.showDetail
}));
+ this.props.clearHeightCache && this.props.clearHeightCache();
}
render() {
- const {data, root, comment} = this.props;
+ const {data, root, comment, clearHeightCache} = this.props;
const {showDetail} = this.state;
const queryData = {
root,
@@ -44,12 +45,14 @@ class CommentDetails extends Component {
<%= t('email.confirm.has_been_requested') %> <%= email %>.
-<%= t('email.confirm.to_confirm') %> Confirm Email
+<%= t('email.confirm.to_confirm') %> <%= t('email.confirm.confirm_email') %>
<%= t('email.confirm.if_you_did_not') %>
diff --git a/services/i18n.js b/services/i18n.js index 4c65ed1d4..cd1bacd93 100644 --- a/services/i18n.js +++ b/services/i18n.js @@ -1,57 +1,91 @@ -const has = require('lodash/has'); -const get = require('lodash/get'); - -const yaml = require('yamljs'); - -const da = yaml.load('./locales/da.yml'); -const es = yaml.load('./locales/es.yml'); -const en = yaml.load('./locales/en.yml'); -const fr = yaml.load('./locales/fr.yml'); -const pt_BR = yaml.load('./locales/pt_BR.yml'); - +const fs = require('fs'); +const path = require('path'); +const debug = require('debug')('talk:services:i18n'); const accepts = require('accepts'); +const _ = require('lodash'); +const yaml = require('yamljs'); +const plugins = require('./plugins'); +const {DEFAULT_LANG} = require('../config'); -// default language -let defaultLanguage = 'en'; -let language = defaultLanguage; -const languages = ['en', 'da', 'es', 'fr', 'pt_BR']; +const resolve = (...paths) => path.resolve(path.join(__dirname, '..', 'locales', ...paths)); -const translations = Object.assign(en, es, fr, pt_BR, da); +// Load all the translations. +let translations = fs.readdirSync(resolve()) + + // Resolve all the filenames relative the the locales directory. + .map((filename) => resolve(filename)) + + // Translations are only yml/yaml files. + .filter((filename) => /\.(yaml|yml)$/.test(filename)) + + // Load the translation files from disk. + .map((filename) => fs.readFileSync(filename, 'utf8')) + + // Load the translation files. + .reduce((packs, contents) => { + + const pack = yaml.parse(contents); + + return _.merge(packs, pack); + }, {}); + +// Create a list of all supported translations. +const languages = Object.keys(translations); + +let loadedPluginTranslations = false; +const loadPluginTranslations = () => { + if (loadedPluginTranslations) { + return; + } + + // Load the plugin translations. + plugins.get('server', 'translations').forEach(({plugin, translations: filename}) => { + debug(`added plugin '${plugin.name}'`); + + const pack = yaml.parse(fs.readFileSync(filename, 'utf8')); + + translations = _.merge(translations, pack); + }); + + loadedPluginTranslations = true; +}; + +const t = (language) => (key, ...replacements) => { + + // Loads the translations into the translations array from plugins. This is + // done lazily to ensure that we don't have an import cycle. + loadPluginTranslations(); + + // Check if the translation exists on the object. + if (_.has(translations[language], key)) { + + // Get the translation value. + let translation = _.get(translations[language], key); + + // Replace any {n} with the arguments passed to this method. + replacements.forEach((str, n) => { + translation = translation.replace(new RegExp(`\\{${n}\\}`, 'g'), str); + }); + + return translation; + } else { + console.warn(`${key} language key not set`); + return key; + } +}; /** * Exposes a service object to allow translations. * @type {Object} */ const i18n = { - - /** - * Create the new Task kue. - */ - init(req) { + request(req) { const lang = accepts(req).language(languages); - language = lang ? lang : defaultLanguage; - }, - - /** - * Translates a key. - */ - t(key, ...replacements) { - - if (has(translations[language], key)) { - - let translation = get(translations[language], key); - - // replace any {n} with the arguments passed to this method - replacements.forEach((str, i) => { - translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str); - }); - - return translation; - } else { - console.warn(`${key} language key not set`); - return key; - } + const language = lang ? lang : DEFAULT_LANG; + + return t(language); }, + t: t(DEFAULT_LANG), }; module.exports = i18n; diff --git a/services/kue.js b/services/kue.js index 01806632e..a3eb6d985 100644 --- a/services/kue.js +++ b/services/kue.js @@ -9,7 +9,8 @@ const kue = require('kue'); // singleton Queue instance. So you can configure and use only a single Queue // object within your node.js process. let queue = null; -const getQueue = () => { +let isManaging = false; +const getQueue = ({managed = false} = {}) => { if (queue) { return queue; } @@ -21,8 +22,16 @@ const getQueue = () => { } }); - // Watch for stuck jobs to manage. - queue.watchStuckJobs(1000); + // If this is a managed queue, and we aren't managing yet, then start the + // management. + if (managed && !isManaging) { + + // Watch for stuck jobs to manage. + queue.watchStuckJobs(60000); + + // Mark that we've now started management routines. + isManaging = true; + } return queue; }; @@ -67,7 +76,9 @@ class Task { * Process jobs for the queue. */ process(callback) { - return getQueue().process(this.name, callback); + + // Get the queue in managed mode. + return getQueue({managed: true}).process(this.name, callback); } /** diff --git a/services/mailer.js b/services/mailer.js index a4f694b30..4433b9ef5 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -13,7 +13,8 @@ const { SMTP_USERNAME, SMTP_PORT, SMTP_PASSWORD, - SMTP_FROM_ADDRESS + SMTP_FROM_ADDRESS, + EMAIL_SUBJECT_PREFIX, } = require('../config'); // load all the templates as strings @@ -95,12 +96,12 @@ const mailer = module.exports = { } // Prefix the subject with `[Talk]`. - subject = `[Talk] ${subject}`; + subject = `${EMAIL_SUBJECT_PREFIX} ${subject}`; attachLocals(locals); - // Attach the templating function. - locals['t'] = i18n.t; + // Attach the translation function. + locals.t = i18n.t; return Promise.all([ @@ -112,7 +113,7 @@ const mailer = module.exports = { ]) .then(([html, text]) => { - // Create the job. + // Create the job. return mailer.task.create({ title: 'Mail', message: { diff --git a/services/settings.js b/services/settings.js index e8537c9c9..72915f43b 100644 --- a/services/settings.js +++ b/services/settings.js @@ -2,6 +2,7 @@ const SettingModel = require('../models/setting'); const cache = require('./cache'); const errors = require('../errors'); const {dotize} = require('./utils'); +const {SETTINGS_CACHE_TIME} = require('../config'); /** * The selector used to uniquely identify the settings document. @@ -35,7 +36,7 @@ module.exports = class SettingsService { if (process.env.NODE_ENV === 'production') { // When in production, wrap the settings retrieval with a cache. - const settings = await cache.h.wrap('settings', fields, 60, () => retrieve(fields)); + const settings = await cache.h.wrap('settings', fields, SETTINGS_CACHE_TIME / 1000, () => retrieve(fields)); return new SettingModel(settings); } diff --git a/services/users.js b/services/users.js index 52cd76dc2..1a8ea7684 100644 --- a/services/users.js +++ b/services/users.js @@ -12,13 +12,9 @@ const { } = require('./events/constants'); const events = require('./events'); -const { - ROOT_URL -} = require('../config'); +const {ROOT_URL} = require('../config'); -const { - jwt: JWT_SECRET -} = require('../secrets'); +const {jwt: JWT_SECRET} = require('../secrets'); const debug = require('debug')('talk:services:users'); @@ -43,12 +39,15 @@ const SALT_ROUNDS = 10; // Create a redis client to use for authentication. const Limit = require('./limit'); -const loginRateLimiter = new Limit('loginAttempts', RECAPTCHA_INCORRECT_TRIGGER, RECAPTCHA_WINDOW); +const loginRateLimiter = new Limit( + 'loginAttempts', + RECAPTCHA_INCORRECT_TRIGGER, + RECAPTCHA_WINDOW +); // UsersService is the interface for the application to interact with the // UserModel through. class UsersService { - /** * Returns a user (if found) for the given email address. */ @@ -57,9 +56,9 @@ class UsersService { profiles: { $elemMatch: { id: email.toLowerCase(), - provider: 'local' - } - } + provider: 'local', + }, + }, }); } @@ -85,22 +84,26 @@ class UsersService { } static async setSuspensionStatus(id, until, assignedBy = null, message) { - let user = await UserModel.findOneAndUpdate({id}, { - $set: { - 'status.suspension.until': until + let user = await UserModel.findOneAndUpdate( + {id}, + { + $set: { + 'status.suspension.until': until, + }, + $push: { + 'status.suspension.history': { + until, + assigned_by: assignedBy, + message, + created_at: Date.now(), + }, + }, }, - $push: { - 'status.suspension.history': { - until, - assigned_by: assignedBy, - message, - created_at: Date.now() - } + { + new: true, + runValidators: true, } - }, { - new: true, - runValidators: true - }); + ); if (user === null) { user = await UserModel.findOne({id}); if (user === null) { @@ -112,45 +115,53 @@ class UsersService { // check if the date is within 1 second of the time we're trying to set. if ( user.status.suspension.until === until || - ( - user.status.suspension.until.getTime() > until.getTime() - 1000 && - user.status.suspension.until.getTime() < until.getTime() + 1000 - ) + (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'); + 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, message, assignedBy}); + await events.emitAsync(USERS_SUSPENSION_CHANGE, user, { + until, + message, + assignedBy, + }); return user; } static async setBanStatus(id, status, assignedBy = null, message) { - let user = await UserModel.findOneAndUpdate({ - id, - status: { - $ne: status - } - }, { - $set: { - 'status.banned.status': status + let user = await UserModel.findOneAndUpdate( + { + id, + status: { + $ne: status, + }, }, - $push: { - 'status.banned.history': { - status, - assigned_by: assignedBy, - message, - created_at: Date.now() - } + { + $set: { + 'status.banned.status': status, + }, + $push: { + 'status.banned.history': { + status, + assigned_by: assignedBy, + message, + created_at: Date.now(), + }, + }, + }, + { + new: true, + runValidators: true, } - }, { - new: true, - runValidators: true - }); + ); if (user === null) { user = await UserModel.findOne({id}); if (user === null) { @@ -165,31 +176,39 @@ class UsersService { } // Emit that the user ban status was changed. - await events.emitAsync(USERS_BAN_CHANGE, user, {status, assignedBy, message}); + await events.emitAsync(USERS_BAN_CHANGE, user, { + status, + assignedBy, + message, + }); return user; } static async setUsernameStatus(id, status, assignedBy = null) { - let user = await UserModel.findOneAndUpdate({ - id, - status: { - $ne: status - } - }, { - $set: { - 'status.username.status': status + let user = await UserModel.findOneAndUpdate( + { + id, + status: { + $ne: status, + }, }, - $push: { - 'status.username.history': { - status, - assigned_by: assignedBy, - created_at: Date.now() - } + { + $set: { + 'status.username.status': status, + }, + $push: { + 'status.username.history': { + status, + assigned_by: assignedBy, + created_at: Date.now(), + }, + }, + }, + { + new: true, } - }, { - new: true - }); + ); if (user === null) { user = await UserModel.findOne({id}); if (user === null) { @@ -200,41 +219,57 @@ class UsersService { return user; } - throw new Error('username status change edit failed for an unknown reason'); + 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, assignedBy}); + await events.emitAsync(USERS_USERNAME_STATUS_CHANGE, user, { + status, + assignedBy, + }); return user; } - static async _setUsername(id, username, fromStatus, toStatus, assignedBy, resetAllowed = false) { + static async _setUsername( + id, + username, + fromStatus, + toStatus, + assignedBy, + resetAllowed = false + ) { try { const query = { id, - 'status.username.status': fromStatus + '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, + let user = await UserModel.findOneAndUpdate( + query, + { + $set: { + username, + lowercaseUsername: username.toLowerCase(), + 'status.username.status': toStatus, + }, + $push: { + 'status.username.history': { + status: toStatus, + assigned_by: assignedBy, + created_at: Date.now(), + }, + }, }, - $push: { - 'status.username.history': { - status: toStatus, - assigned_by: assignedBy, - created_at: Date.now() - } + { + new: true, } - }, { - new: true - }); + ); if (!user) { user = await UsersService.findById(id); if (user === null) { @@ -266,11 +301,24 @@ class UsersService { } static async setUsername(id, username, assignedBy) { - return UsersService._setUsername(id, username, 'UNSET', 'SET', assignedBy, true); + return UsersService._setUsername( + id, + username, + 'UNSET', + 'SET', + assignedBy, + true + ); } static async changeUsername(id, username, assignedBy) { - return UsersService._setUsername(id, username, 'REJECTED', 'CHANGED', assignedBy); + return UsersService._setUsername( + id, + username, + 'REJECTED', + 'CHANGED', + assignedBy + ); } /** @@ -294,18 +342,21 @@ class UsersService { * Sets or unsets the recaptcha_required flag on a user's local profile. */ static flagForRecaptchaRequirement(email, required) { - return UserModel.update({ - profiles: { - $elemMatch: { - id: email.toLowerCase(), - provider: 'local' - } + return UserModel.update( + { + profiles: { + $elemMatch: { + id: email.toLowerCase(), + provider: 'local', + }, + }, + }, + { + $set: { + 'profiles.$.metadata.recaptcha_required': required, + }, } - }, { - $set: { - 'profiles.$.metadata.recaptcha_required': required - } - }); + ); } /** @@ -320,11 +371,10 @@ class UsersService { static mergeUsers(dstUserID, srcUserID) { let srcUser, dstUser; - return Promise - .all([ - UserModel.findOne({id: dstUserID}).exec(), - UserModel.findOne({id: srcUserID}).exec() - ]) + return Promise.all([ + UserModel.findOne({id: dstUserID}).exec(), + UserModel.findOne({id: srcUserID}).exec(), + ]) .then((users) => { dstUser = users[0]; srcUser = users[1]; @@ -353,9 +403,9 @@ class UsersService { profiles: { $elemMatch: { id, - provider - } - } + provider, + }, + }, }); if (user) { return user; @@ -375,10 +425,10 @@ class UsersService { username: { status: 'UNSET', history: { - status: 'UNSET' - } - } - } + status: 'UNSET', + }, + }, + }, }); // Save the user in the database. @@ -396,22 +446,28 @@ class UsersService { * @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); + let token = await UsersService.createEmailConfirmToken( + user, + email, + redirectURI + ); return MailerService.sendSimple({ template: 'email-confirm', locals: { token, rootURL: ROOT_URL, - email + email, }, subject: i18n.t('email.confirm.subject'), - to: email + to: email, }); } static async sendEmail(user, options) { - const localProfile = user.profiles.find((profile) => profile.provider === 'local'); + const localProfile = user.profiles.find( + (profile) => profile.provider === 'local' + ); if (!localProfile) { throw new Error('user does not have an email'); } @@ -428,12 +484,15 @@ class UsersService { static async changePassword(id, password) { const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); - return UserModel.update({id}, { - $inc: {__v: 1}, - $set: { - password: hashedPassword + return UserModel.update( + {id}, + { + $inc: {__v: 1}, + $set: { + password: hashedPassword, + }, } - }); + ); } /** @@ -442,10 +501,15 @@ class UsersService { * @return {Promise} Resolves with the users that were created */ static createLocalUsers(users) { - return Promise.all(users.map((user) => { - return UsersService - .createLocalUser(user.email, user.password, user.username); - })); + return Promise.all( + users.map((user) => { + return UsersService.createLocalUser( + user.email, + user.password, + user.username + ); + }) + ); } /** @@ -466,7 +530,6 @@ class UsersService { } if (checkAgainstWordlist) { - // check for profanity let err = await Wordlist.usernameCheck(username); if (err) { @@ -501,7 +564,6 @@ class UsersService { * @param {Function} done callback */ static async createLocalUser(email, password, username) { - if (!email) { throw errors.ErrMissingEmail; } @@ -511,7 +573,7 @@ class UsersService { await Promise.all([ UsersService.isValidUsername(username), - UsersService.isValidPassword(password) + UsersService.isValidPassword(password), ]); const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); @@ -523,17 +585,17 @@ class UsersService { profiles: [ { id: email, - provider: 'local' - } + provider: 'local', + }, ], status: { username: { status: 'SET', history: { - status: 'SET' - } - } - } + status: 'SET', + }, + }, + }, }); try { @@ -566,7 +628,7 @@ class UsersService { /** * Finds a user with the id. * @param {String} id user id (uuid) - */ + */ static findById(id) { return UserModel.findOne({id}); } @@ -577,13 +639,11 @@ class UsersService { * @param {Object} token a jwt token used to sign in the user */ static async findOrCreateByIDToken(id, token) { - // Try to get the user. let user = await UserModel.findOne({id}); // If the user was not found, try to look it up. if (user === null) { - // If the user wasn't found, it will return null and the variable will be // unchanged. user = await lookupUserNotFound(token); @@ -595,21 +655,24 @@ class UsersService { /** * Finds users in an array of ids. * @param {Array} ids array of user identifiers (uuid) - */ + */ static findByIdArray(ids) { return UserModel.find({ - id: {$in: ids} + id: {$in: ids}, }); } /** * Finds public user information by an array of ids. * @param {Array} ids array of user identifiers (uuid) - */ + */ static findPublicByIdArray(ids) { - return UserModel.find({ - id: {$in: ids} - }, 'id username'); + return UserModel.find( + { + id: {$in: ids}, + }, + 'id username' + ); } /** @@ -618,7 +681,9 @@ class UsersService { */ static async createPasswordResetToken(email, loc) { 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 passord' + ); } email = email.toLowerCase(); @@ -628,7 +693,6 @@ class UsersService { DomainList.urlCheck(loc), ]); if (!user) { - // Since we don't want to reveal that the email does/doesn't exist // just go ahead and resolve the Promise with null and check in the // endpoint. @@ -646,12 +710,12 @@ class UsersService { email, loc, userId: user.id, - version: user.__v + version: user.__v, }; return JWT_SECRET.sign(payload, { expiresIn: '1d', - subject: PASSWORD_RESET_JWT_SUBJECT + subject: PASSWORD_RESET_JWT_SUBJECT, }); } @@ -678,7 +742,7 @@ class UsersService { */ static async verifyPasswordResetToken(token) { const {userId, loc, version} = await UsersService.verifyToken(token, { - subject: PASSWORD_RESET_JWT_SUBJECT + subject: PASSWORD_RESET_JWT_SUBJECT, }); const user = await UsersService.findById(userId); @@ -708,17 +772,16 @@ class UsersService { return UserModel.find({ $or: [ - // Search by a prefix match on the username. { - 'lowercaseUsername': { + lowercaseUsername: { $regex, }, }, // Search by a prefix match on the email address. { - 'profiles': { + profiles: { $elemMatch: { id: { $regex, @@ -752,13 +815,16 @@ class UsersService { * @return {Promise} */ static updateSettings(id, settings) { - return UserModel.update({ - id - }, { - $set: { - settings + return UserModel.update( + { + id, + }, + { + $set: { + settings, + }, } - }); + ); } /** @@ -774,7 +840,7 @@ class UsersService { item_type: 'users', user_id, action_type, - metadata + metadata, }); } @@ -787,7 +853,9 @@ 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 password'); + throw new Error( + 'email is required when creating a JWT for resetting password' + ); } // Conform the email to lowercase. @@ -796,22 +864,27 @@ class UsersService { const tokenOptions = { jwtid: uuid.v4(), expiresIn: '1d', - subject: EMAIL_CONFIRM_JWT_SUBJECT + subject: EMAIL_CONFIRM_JWT_SUBJECT, }; // Get the profile representing the local account. - let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local'); + 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) { throw new Error('email address already confirmed'); } - return JWT_SECRET.sign({ - email, - referer, - userID: user.id - }, tokenOptions); + return JWT_SECRET.sign( + { + email, + referer, + userID: user.id, + }, + tokenOptions + ); } /** @@ -823,7 +896,7 @@ class UsersService { */ static async verifyEmailConfirmation(token) { let {userID, email, referer} = await UsersService.verifyToken(token, { - subject: EMAIL_CONFIRM_JWT_SUBJECT + subject: EMAIL_CONFIRM_JWT_SUBJECT, }); await UsersService.confirmEmail(userID, email); @@ -835,20 +908,22 @@ class UsersService { * Marks the email on the user as confirmed. */ static confirmEmail(id, email) { - return UserModel - .update({ + return UserModel.update( + { id, profiles: { $elemMatch: { id: email, - provider: 'local' - } - } - }, { + provider: 'local', + }, + }, + }, + { $set: { - 'profiles.$.metadata.confirmed_at': new Date() - } - }); + 'profiles.$.metadata.confirmed_at': new Date(), + }, + } + ); } /** @@ -866,13 +941,16 @@ class UsersService { throw errors.ErrCannotIgnoreStaff; } - return UserModel.update({id}, { - $addToSet: { - ignoresUsers: { - $each: usersToIgnore - } + return UserModel.update( + {id}, + { + $addToSet: { + ignoresUsers: { + $each: usersToIgnore, + }, + }, } - }); + ); } /** @@ -881,24 +959,26 @@ class UsersService { * @param {Array