diff --git a/config.js b/config.js index cb9d6c84d..f9bf97740 100644 --- a/config.js +++ b/config.js @@ -27,7 +27,14 @@ const CONFIG = { // WEBPACK indicates when webpack is currently building. WEBPACK: process.env.WEBPACK === 'TRUE', + // APOLLO_ENGINE_KEY specifies the key used to connect Talk to + // https://engine.apollo.com/ for tracing of GraphQL requests. + // + // Note: Apollo Engine is a premium service, may not be free for certain + // volumes of queries. APOLLO_ENGINE_KEY: process.env.APOLLO_ENGINE_KEY || null, + + // ENABLE_TRACING is true when the APOLLO_ENGINE_KEY is provided. ENABLE_TRACING: Boolean(process.env.APOLLO_ENGINE_KEY), // EMAIL_SUBJECT_PREFIX is the string before emails in the subject. diff --git a/graph/loaders/users.js b/graph/loaders/users.js index 665690202..679d3dea5 100644 --- a/graph/loaders/users.js +++ b/graph/loaders/users.js @@ -8,6 +8,7 @@ const { } = require('../../perms/constants'); const UsersService = require('../../services/users'); +const {escapeRegExp} = require('../../services/regex'); const UserModel = require('../../models/user'); const mergeState = (query, state) => { @@ -72,14 +73,48 @@ 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}, {ids, limit, cursor, state, action_type, sortOrder}) => { +const getUsersByQuery = async ({user}, {limit, cursor, value = '', state, action_type, sortOrder}) => { let query = UserModel.find(); - if (action_type || state) { + if (action_type || state || value.length > 0) { if (!user || !user.can(SEARCH_OTHER_USERS)) { return null; } + if (value.length > 0) { + + // Lowercase the search term and escape any regex characters. + value = escapeRegExp(value).toLowerCase(); + + // Compile the prefix search regex. + const $regex = new RegExp(`^${value}`); + + // Merge in the regex params. + query.merge({ + $or: [ + + // Search by a prefix match on the username. + { + lowercaseUsername: { + $regex, + }, + }, + + // Search by a prefix match on the email address. + { + profiles: { + $elemMatch: { + id: { + $regex, + }, + provider: 'local', + }, + }, + }, + ], + }); + } + if (state) { mergeState(query, state); } @@ -93,14 +128,6 @@ const getUsersByQuery = async ({user}, {ids, limit, cursor, state, action_type, } } - if (ids) { - query = query.find({ - id: { - $in: ids - } - }); - } - if (cursor) { if (sortOrder === 'DESC') { query = query.where({ @@ -125,6 +152,7 @@ const getUsersByQuery = async ({user}, {ids, limit, cursor, state, action_type, // Sort by created_at. query.sort({created_at: sortOrder === 'DESC' ? -1 : 1}); + // Execute the query. const nodes = await query.exec(); // The hasNextPage is always handled the same (ask for one more than we need, diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index aa8b90f20..7299efc3a 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -78,7 +78,7 @@ const RootQuery = { // This endpoint is used for loading the user moderation queues (users whose username has been flagged), // so hide it in the event that we aren't an admin. - async users(_, {query}, {user, loaders: {Users}}) { + users(_, {query}, {user, loaders: {Users}}) { if (user == null || !user.can(SEARCH_OTHER_USERS)) { return null; } diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index 7c66bb471..cfc72e6e4 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -50,7 +50,7 @@ const User = { return tokens; }, - async ignoredUsers({id}, args, {user, loaders: {Users}}) { + ignoredUsers({id}, args, {user, loaders: {Users}}) { // Only allow a logged in user that is either the current user or is a staff // member to access the ignoredUsers of a given user. @@ -63,8 +63,7 @@ const User = { return []; } - const connection = await Users.getByQuery({ids: user.ignoresUsers}); - return connection.nodes; + return Users.getByID.loadMany(user.ignoresUsers); }, role({id, role}, _, {user}) { diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index a8cd9e502..7d39125e0 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -233,8 +233,12 @@ input UsersQuery { # Users returned will only be ones which have at least one action of this. action_type: ACTION_TYPE + # state will filter the users to a specific set of users that meet. state: UserStateInput + # value is the search string to use to search for a pa + value: String = "" + # Limit the number of results to be returned. limit: Int = 10 diff --git a/routes/api/users/index.js b/routes/api/users/index.js index 4832847c6..fd71fecda 100644 --- a/routes/api/users/index.js +++ b/routes/api/users/index.js @@ -5,56 +5,6 @@ const errors = require('../../../errors'); const authorization = require('../../../middleware/authorization'); const Limit = require('../../../services/limit'); -router.get('/', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => { - - const { - value = '', - field = 'created_at', - page = 1, - asc = 'false', - limit = 20 // Total Per Page - } = req.query; - - try { - - const queryOpts = { - sort: {[field]: (asc === 'true') ? 1 : -1}, - skip: (page - 1) * limit, - limit - }; - - let [result, count] = await Promise.all([ - UsersService - .search(value) - .sort(queryOpts.sort) - .skip(parseInt(queryOpts.skip)) - .limit(parseInt(queryOpts.limit)) - .lean(), - UsersService.search(value).count() - ]); - - res.json({ - result, - limit: Number(limit), - count, - page: Number(page), - totalPages: Math.ceil(count / (limit === 0 ? 1 : limit)) - }); - - } catch (e) { - next(e); - } -}); - -router.post('/:user_id/role', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => { - try { - await UsersService.setRole(req.params.user_id, req.body.role); - res.status(204).end(); - } catch (e) { - next(e); - } -}); - // create a local user. router.post('/', async (req, res, next) => { const {email, password, username} = req.body; diff --git a/services/users.js b/services/users.js index 3d70eeafd..c0fc47d9b 100644 --- a/services/users.js +++ b/services/users.js @@ -28,7 +28,6 @@ const MailerService = require('./mailer'); const i18n = require('./i18n'); const Wordlist = require('./wordlist'); const DomainList = require('./domain_list'); -const {escapeRegExp} = require('./regex'); const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm'; const PASSWORD_RESET_JWT_SUBJECT = 'password_reset'; @@ -759,47 +758,6 @@ class UsersService { return [user, loc]; } - /** - * Finds a user using a value which gets compared using a prefix match against - * the user's email address and/or their username. - * @param {String} value value to search by - * @return {Promise} - */ - static search(value) { - if (!value || typeof value !== 'string' || value.length === 0) { - return UserModel.find({}); - } - - value = escapeRegExp(value).toLowerCase(); - - // Compile the prefix search regex. - const $regex = new RegExp(`^${value}`); - - return UserModel.find({ - $or: [ - - // Search by a prefix match on the username. - { - lowercaseUsername: { - $regex, - }, - }, - - // Search by a prefix match on the email address. - { - profiles: { - $elemMatch: { - id: { - $regex, - }, - provider: 'local', - }, - }, - }, - ], - }); - } - /** * Returns a count of the current users. * @return {Promise} diff --git a/test/server/services/users.js b/test/server/services/users.js index 63435766d..dffbc0ee6 100644 --- a/test/server/services/users.js +++ b/test/server/services/users.js @@ -179,46 +179,6 @@ describe('services.UsersService', () => { }); }); - describe('#search', () => { - it('should return all the results without a value', async () => { - expect(await UsersService.search()).to.have.length(3); - }); - - it('should match the search terms', async () => { - const tests = [ - { - search: 'Stamp', - results: 1, - id: mockUsers[0].id, - }, - { - search: 'sockmonster', - results: 1, - id: mockUsers[1].id, - }, - { - search: 'marvel', - results: 1, - id: mockUsers[2].id, - }, - { - search: 'marvel', - results: 1, - id: mockUsers[2].id, - }, - ]; - - for (const test of tests) { - const users = await UsersService.search(test.search); - - expect(users).to.have.length(test.results); - if (test.results === 1) { - expect(users[0]).to.have.property('id', test.id); - } - } - }); - }); - [ {func: 'changeUsername', okStatus: 'REJECTED', notOKStatus: 'UNSET', newStatus: 'CHANGED'}, {func: 'setUsername', okStatus: 'UNSET', notOKStatus: 'REJECTED', newStatus: 'SET'},