diff --git a/.gitignore b/.gitignore index 811e2ea2b..7c0007dc9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ client/coral-framework/graphql/introspection.json *.swp *.DS_STORE .prettierrc.json +.vscode coverage/ test/e2e/reports/ diff --git a/bin/cli-users b/bin/cli-users index 950f2a538..cfcbcd13a 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -7,8 +7,6 @@ const util = require('./util'); const program = require('commander'); const inquirer = require('inquirer'); -const { graphql } = require('graphql'); -const helpers = require('../services/migration/helpers'); const { stripIndent } = require('common-tags'); const Table = require('cli-table'); @@ -21,31 +19,15 @@ inquirer.registerPrompt( require('inquirer-autocomplete-prompt') ); -const schema = require('../graph/schema'); const Context = require('../graph/context'); const UsersService = require('../services/users'); const UserModel = require('../models/user'); -const CommentModel = require('../models/comment'); -const ActionModel = require('../models/action'); const USER_ROLES = require('../models/enum/user_roles'); const mongoose = require('../services/mongoose'); // Register the shutdown criteria. util.onshutdown([() => mongoose.disconnect()]); -/** - * transforms a specific action to a removal action on the target model. - */ -const actionDecrTransformer = ({ item_id, action_type, group_id }) => ({ - query: { id: item_id }, - update: { - $inc: { - [`action_counts.${action_type.toLowerCase()}`]: -1, - [`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`]: -1, - }, - }, -}); - /** * Deletes a user and cleans up their associated verifications. */ @@ -76,66 +58,29 @@ async function deleteUser(userID) { return util.shutdown(); } - const { transformSingleWithCursor } = helpers({ - queryBatchSize: 10000, - updateBatchSize: 10000, - }); + const ctx = Context.forSystem(); - console.warn("Removing user's actions"); - - // Remove all actions against comments. - await transformSingleWithCursor( - ActionModel.collection.find({ user_id: user.id, item_type: 'COMMENTS' }), - actionDecrTransformer, - CommentModel + const { data, errors } = await ctx.graphql( + ` + mutation DeleteUser($user_id: ID!) { + delUser(id: $user_id) { + errors { + translation_key + } + } + } + `, + { user_id: user.id } ); + if (errors) { + throw errors; + } - // Remove all actions against users. - await transformSingleWithCursor( - ActionModel.collection.find({ user_id: user.id, item_type: 'USERS' }), - actionDecrTransformer, - UserModel - ); + if (data.errors) { + throw data.errors; + } - // Remove all the user's actions. - await ActionModel.where({ user_id: user.id }) - .setOptions({ multi: true }) - .remove(); - - console.warn("Removing user's comments"); - - // Removes all the user's reply counts on each of the comments that they - // have commented on. - await transformSingleWithCursor( - CommentModel.collection.aggregate([ - { $match: { author_id: user.id } }, - { - $group: { - _id: '$parent_id', - count: { $sum: 1 }, - }, - }, - ]), - ({ _id: parent_id, count }) => ({ - query: { id: parent_id }, - update: { - $inc: { - reply_count: -1 * count, - }, - }, - }), - CommentModel - ); - - // Remove all the user's comments. - await CommentModel.where({ author_id: user.id }) - .setOptions({ multi: true }) - .remove(); - - console.warn('Removing the user'); - - // Remove the user. - await user.remove(); + console.log('User was deleted.'); util.shutdown(); } catch (err) { @@ -197,7 +142,7 @@ async function searchUsers() { value = ''; } - const { data, errors } = await graphql(schema, searchQuery, {}, ctx, { + const { data, errors } = await ctx.graphql(searchQuery, { value, }); if (errors && errors.length > 0) { diff --git a/client/coral-admin/src/components/UserDetailComment.js b/client/coral-admin/src/components/UserDetailComment.js index 3f09905d9..72c85c960 100644 --- a/client/coral-admin/src/components/UserDetailComment.js +++ b/client/coral-admin/src/components/UserDetailComment.js @@ -75,7 +75,8 @@ class UserDetailComment extends React.Component {
- Story: {comment.asset.title} + {t('common.story')}:{' '} + {comment.asset.title ? comment.asset.title : comment.asset.url} { {t('modqueue.moderate')} @@ -106,6 +107,7 @@ class UserDetailComment extends React.Component {
+ {/* TODO: translate string */} Contains Link diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index 62aaad645..bc5780c0a 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -125,7 +125,8 @@ class Comment extends React.Component {
- Story: {comment.asset.title} + {t('common.story')}:{' '} + {comment.asset.title ? comment.asset.title : comment.asset.url} {!currentAsset && ( {t('modqueue.moderate')} @@ -156,6 +157,7 @@ class Comment extends React.Component {
+ {/* TODO: translate string */} Contains Link diff --git a/graph/mutators/user.js b/graph/mutators/user.js index f8db7a19e..9d31e6dbd 100644 --- a/graph/mutators/user.js +++ b/graph/mutators/user.js @@ -1,5 +1,6 @@ const errors = require('../../errors'); const UsersService = require('../../services/users'); +const migrationHelpers = require('../../services/migration/helpers'); const { CHANGE_USERNAME, SET_USERNAME, @@ -7,6 +8,7 @@ const { SET_USER_BAN_STATUS, SET_USER_SUSPENSION_STATUS, UPDATE_USER_ROLES, + DELETE_USER, } = require('../../perms/constants'); const setUserUsernameStatus = async (ctx, id, status) => { @@ -70,6 +72,87 @@ const setRole = (ctx, id, role) => { return UsersService.setRole(id, role); }; +/** + * transforms a specific action to a removal action on the target model. + */ +const actionDecrTransformer = ({ item_id, action_type, group_id }) => ({ + query: { id: item_id }, + update: { + $inc: { + [`action_counts.${action_type.toLowerCase()}`]: -1, + [`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`]: -1, + }, + }, +}); + +// delUser will delete a given user with the specified id. +const delUser = async (ctx, id) => { + const { connectors: { models: { User, Action, Comment } } } = ctx; + + // Find the user we're removing. + const user = await User.findOne({ id }); + if (!user) { + throw errors.ErrNotFound; + } + + // Get the query transformer we'll use to help batch process the user + // deletion. + const { transformSingleWithCursor } = migrationHelpers({ + queryBatchSize: 10000, + updateBatchSize: 10000, + }); + + // Remove all actions against comments. + await transformSingleWithCursor( + Action.collection.find({ user_id: user.id, item_type: 'COMMENTS' }), + actionDecrTransformer, + Comment + ); + + // Remove all actions against users. + await transformSingleWithCursor( + Action.collection.find({ user_id: user.id, item_type: 'USERS' }), + actionDecrTransformer, + User + ); + + // Remove all the user's actions. + await Action.where({ user_id: user.id }) + .setOptions({ multi: true }) + .remove(); + + // Removes all the user's reply counts on each of the comments that they + // have commented on. + await transformSingleWithCursor( + Comment.collection.aggregate([ + { $match: { author_id: user.id } }, + { + $group: { + _id: '$parent_id', + count: { $sum: 1 }, + }, + }, + ]), + ({ _id: parent_id, count }) => ({ + query: { id: parent_id }, + update: { + $inc: { + reply_count: -1 * count, + }, + }, + }), + Comment + ); + + // Remove all the user's comments. + await Comment.where({ author_id: user.id }) + .setOptions({ multi: true }) + .remove(); + + // Remove the user. + await user.remove(); +}; + module.exports = ctx => { let mutators = { User: { @@ -81,6 +164,7 @@ module.exports = ctx => { setUserUsernameStatus: () => Promise.reject(errors.ErrNotAuthorized), setUsername: () => Promise.reject(errors.ErrNotAuthorized), stopIgnoringUser: () => Promise.reject(errors.ErrNotAuthorized), + del: () => Promise.reject(errors.ErrNotAuthorized), }, }; @@ -116,6 +200,10 @@ module.exports = ctx => { mutators.User.setUserSuspensionStatus = (id, until, message) => setUserSuspensionStatus(ctx, id, until, message); } + + if (ctx.user.can(DELETE_USER)) { + mutators.User.del = id => delUser(ctx, id); + } } return mutators; diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 3757bb3c4..2838f0f99 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -136,6 +136,9 @@ const RootMutation = { forceScrapeAsset: async (_, { id }, { mutators: { Asset } }) => { await Asset.scrape(id); }, + delUser: async (_, { id }, { mutators: { User } }) => { + await User.del(id); + }, }; module.exports = RootMutation; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index ea574ee6b..15b888ef1 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -1426,6 +1426,12 @@ type ForceScrapeAssetResponse implements Response { errors: [UserError!] } +type DelUserResponse 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 { @@ -1523,6 +1529,9 @@ type RootMutation { # forceScrapeAsset will force scrape the Asset with the given ID. forceScrapeAsset(id: ID!): ForceScrapeAssetResponse + + # delUser will delete the user with the specified id. + delUser(id: ID!): DelUserResponse } type UsernameChangedPayload { diff --git a/perms/constants/mutation.js b/perms/constants/mutation.js index 06e5b6f0f..d2ebe73f1 100644 --- a/perms/constants/mutation.js +++ b/perms/constants/mutation.js @@ -18,4 +18,5 @@ module.exports = { UPDATE_ASSET_SETTINGS: 'UPDATE_ASSET_SETTINGS', UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS', UPDATE_SETTINGS: 'UPDATE_SETTINGS', + DELETE_USER: 'DELETE_USER', }; diff --git a/routes/api/v1/auth.js b/routes/api/v1/auth.js index 369f4278a..d8e0544a9 100644 --- a/routes/api/v1/auth.js +++ b/routes/api/v1/auth.js @@ -4,6 +4,7 @@ const { HandleGenerateCredentials, HandleLogout, } = require('../../../services/passport'); +const authz = require('../../../middleware/authorization'); const router = express.Router(); /** @@ -26,7 +27,7 @@ router.get('/', (req, res, next) => { /** * This blacklists the token used to authenticate. */ -router.delete('/', HandleLogout); +router.delete('/', authz.needed(), HandleLogout); //============================================================================== // PASSPORT ROUTES diff --git a/services/passport.js b/services/passport.js index 59215dd54..5748c911b 100644 --- a/services/passport.js +++ b/services/passport.js @@ -184,24 +184,24 @@ async function ValidateUserLogin(loginProfile, user, done) { * Revoke the token on the request. */ const HandleLogout = async (req, res, next) => { - const { jwt } = req; - - const now = new Date(); - const expiry = (jwt.exp - now.getTime() / 1000).toFixed(0); - try { + const { jwt } = req; + + const now = new Date(); + const expiry = (jwt.exp - now.getTime() / 1000).toFixed(0); + await client().set(`jtir[${jwt.jti}]`, now.toISOString(), 'EX', expiry); + + // Only clear the cookie on logout if enabled. + if (JWT_CLEAR_COOKIE_LOGOUT) { + debug('clearing the login cookie'); + res.clearCookie(JWT_SIGNING_COOKIE_NAME); + } + + res.status(204).end(); } catch (err) { return next(err); } - - // Only clear the cookie on logout if enabled. - if (JWT_CLEAR_COOKIE_LOGOUT) { - debug('clearing the login cookie'); - res.clearCookie(JWT_SIGNING_COOKIE_NAME); - } - - res.status(204).end(); }; const checkGeneralTokenBlacklist = jwt =>