From 76e87f4c343e55e013cde0c424585aa2cee0b993 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 17 May 2017 23:43:16 +0700 Subject: [PATCH] Move previous suspendUser feature to rejectUsername --- .../Community/CommunityContainer.js | 8 +-- .../Community/components/SuspendUserDialog.js | 6 +- .../ModerationQueue/ModerationContainer.js | 31 ++++++--- .../components/SuspendUserDialog.js | 3 +- .../src/graphql/mutations/index.js | 14 ++++ .../graphql/mutations/rejectUsername.graphql | 7 ++ client/coral-admin/src/reducers/moderation.js | 1 + client/coral-framework/actions/auth.js | 4 +- graph/mutators/user.js | 13 +++- graph/resolvers/root_mutation.js | 7 +- graph/typeDefs.graphql | 25 ++++++- models/user.js | 15 ++-- services/users.js | 69 ++++++++++++++----- 13 files changed, 146 insertions(+), 57 deletions(-) create mode 100644 client/coral-admin/src/graphql/mutations/rejectUsername.graphql diff --git a/client/coral-admin/src/containers/Community/CommunityContainer.js b/client/coral-admin/src/containers/Community/CommunityContainer.js index 899958e17..7c0051477 100644 --- a/client/coral-admin/src/containers/Community/CommunityContainer.js +++ b/client/coral-admin/src/containers/Community/CommunityContainer.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; import {compose} from 'react-apollo'; import {modUserFlaggedQuery} from 'coral-admin/src/graphql/queries'; -import {banUser, setUserStatus, suspendUser} from 'coral-admin/src/graphql/mutations'; +import {banUser, setUserStatus, rejectUsername} from 'coral-admin/src/graphql/mutations'; import { fetchAccounts, @@ -113,7 +113,7 @@ class CommunityContainer extends Component { error={data.error} showBanUserDialog={props.showBanUserDialog} approveUser={props.approveUser} - suspendUser={props.suspendUser} + rejectUsername={props.rejectUsername} showSuspendUserDialog={props.showSuspendUserDialog} /> ); @@ -165,5 +165,5 @@ export default compose( modUserFlaggedQuery, banUser, setUserStatus, - suspendUser + rejectUsername )(CommunityContainer); diff --git a/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js index 0841333be..b717d1719 100644 --- a/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js +++ b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js @@ -34,7 +34,7 @@ class SuspendUserDialog extends Component { static propTypes = { stage: PropTypes.number, handleClose: PropTypes.func.isRequired, - suspendUser: PropTypes.func.isRequired + rejectUsername: PropTypes.func.isRequired } componentDidMount() { @@ -46,13 +46,13 @@ class SuspendUserDialog extends Component { * handles the possible actions for that dialog. */ onActionClick = (stage, menuOption) => () => { - const {suspendUser, user} = this.props; + const {rejectUsername, user} = this.props; const {stage} = this.state; const cancel = this.props.handleClose; const next = () => this.setState({stage: stage + 1}); const suspend = () => { - suspendUser({id: user.user.id, message: this.state.email, mustChangeUsername: true}) + rejectUsername({id: user.user.id, message: this.state.email}) .then(() => { this.props.handleClose(); }); diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js index df79adc44..e65551487 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js @@ -10,7 +10,7 @@ import translations from 'coral-admin/src/translations'; import I18n from 'coral-framework/modules/i18n/i18n'; import {modQueueQuery, getQueueCounts} from '../../graphql/queries'; -import {banUser, setCommentStatus} from '../../graphql/mutations'; +import {banUser, setCommentStatus, suspendUser} from '../../graphql/mutations'; import {fetchSettings} from 'actions/settings'; import {updateAssets} from 'actions/assets'; @@ -211,14 +211,24 @@ class ModerationContainer extends Component { { - toast( - lang.t('suspenduser.notify_suspend_until', - moderation.suspendUserDialog.username, - lang.timeago(result.until)), - {type: 'success'} - ); + onPerform={(args) => { + props.suspendUser(args) + .then(() => { + toast( + lang.t('suspenduser.notify_suspend_until', + moderation.suspendUserDialog.username, + lang.timeago(args.until)), + {type: 'success'} + ); + }) + .catch((err) => { + toast( + err, + {type: 'error'} + ); + }); props.hideSuspendUserDialog(); }} /> @@ -238,7 +248,7 @@ const mapStateToProps = (state) => ({ assets: state.assets.get('assets') }); -const mapDispatchToProps = dispatch => ({ +const mapDispatchToProps = (dispatch) => ({ onClose: () => dispatch(toggleModal(false)), hideBanUserDialog: () => dispatch(hideBanUserDialog(false)), ...bindActionCreators({ @@ -257,6 +267,7 @@ export default compose( connect(mapStateToProps, mapDispatchToProps), setCommentStatus, getQueueCounts, + banUser, + suspendUser, modQueueQuery, - banUser )(ModerationContainer); diff --git a/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js index 4a56762bd..292c0909e 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js @@ -40,8 +40,9 @@ class SuspendUserDialog extends React.Component { handlePerform = () => { this.props.onPerform({ - until: dateAdd(new Date(), 'hour', this.state.duration), + id: this.props.userId, message: this.state.message, + until: dateAdd(new Date(), 'hour', this.state.duration), }); }; diff --git a/client/coral-admin/src/graphql/mutations/index.js b/client/coral-admin/src/graphql/mutations/index.js index aad348c5c..0e3b6b25c 100644 --- a/client/coral-admin/src/graphql/mutations/index.js +++ b/client/coral-admin/src/graphql/mutations/index.js @@ -2,6 +2,7 @@ import {graphql} from 'react-apollo'; import SET_USER_STATUS from './setUserStatus.graphql'; import SET_COMMENT_STATUS from './setCommentStatus.graphql'; import SUSPEND_USER from './suspendUser.graphql'; +import REJECT_USERNAME from './rejectUsername.graphql'; export const banUser = graphql(SET_USER_STATUS, { props: ({mutate}) => ({ @@ -43,6 +44,19 @@ export const suspendUser = graphql(SUSPEND_USER, { }) }); +export const rejectUsername = graphql(REJECT_USERNAME, { + props: ({mutate}) => ({ + rejectUsername: (input) => { + return mutate({ + variables: { + input, + }, + refetchQueries: ['Users'] + }); + } + }) +}); + const views = ['all', 'premod', 'flagged', 'accepted', 'rejected']; export const setCommentStatus = graphql(SET_COMMENT_STATUS, { props: ({mutate}) => ({ diff --git a/client/coral-admin/src/graphql/mutations/rejectUsername.graphql b/client/coral-admin/src/graphql/mutations/rejectUsername.graphql new file mode 100644 index 000000000..c07887649 --- /dev/null +++ b/client/coral-admin/src/graphql/mutations/rejectUsername.graphql @@ -0,0 +1,7 @@ +mutation rejectUsername($input: RejectUsernameInput!) { + rejectUsername(input: $input) { + errors { + translation_key + } + } +} diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js index 67fd8700b..ee8825acf 100644 --- a/client/coral-admin/src/reducers/moderation.js +++ b/client/coral-admin/src/reducers/moderation.js @@ -38,6 +38,7 @@ export default function moderation (state = initialState, action) { .mergeDeep({ suspendUserDialog: { show: true, + userId: action.userId, username: action.username, commentId: action.commentId, commentStatus: action.commentStatus, diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 827991cb5..7da153019 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -196,8 +196,8 @@ export const facebookCallback = (err, data) => (dispatch, getState) => { dispatch(handleAuthToken(data.token)); dispatch(signInFacebookSuccess(data.user)); dispatch(hideSignInDialog()); - const {user: {canEditName, suspension}} = getState().auth.toJS(); - if (canEditName && !suspension.mustChangeUsername) { + const {user: {canEditName, status}} = getState().auth.toJS(); + if (canEditName && status !== 'BANNED') { dispatch(showCreateUsernameDialog()); } } catch (err) { diff --git a/graph/mutators/user.js b/graph/mutators/user.js index 84fb8fd41..34cfd4355 100644 --- a/graph/mutators/user.js +++ b/graph/mutators/user.js @@ -5,8 +5,12 @@ const setUserStatus = ({user}, {id, status}) => { return UsersService.setStatus(id, status); }; -const suspendUser = ({user}, {id, message, mustChangeUsername, until}) => { - return UsersService.suspendUser(id, message, mustChangeUsername, until); +const suspendUser = ({user}, {id, message, until}) => { + return UsersService.suspendUser(id, message, until); +}; + +const rejectUsername = ({user}, {id, message}) => { + return UsersService.rejectUsername(id, message); }; const ignoreUser = ({user}, userToIgnore) => { @@ -22,6 +26,7 @@ module.exports = (context) => { 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), } @@ -35,5 +40,9 @@ module.exports = (context) => { mutators.User.suspendUser = (action) => suspendUser(context, action); } + if (context.user && context.user.can('mutation:rejectUsername')) { + mutators.User.rejectUsername = (action) => rejectUsername(context, action); + } + return mutators; }; diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 0b954f2a1..e17cdcc8a 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -20,8 +20,11 @@ const RootMutation = { setUserStatus(_, {id, status}, {mutators: {User}}) { return wrapResponse(null)(User.setUserStatus({id, status})); }, - suspendUser(_, {input: {id, message, mustChangeUsername, until}}, {mutators: {User}}) { - return wrapResponse(null)(User.suspendUser({id, message, mustChangeUsername, until})); + suspendUser(_, {input: {id, message, until}}, {mutators: {User}}) { + return wrapResponse(null)(User.suspendUser({id, message, until})); + }, + rejectUsername(_, {input: {id, message}}, {mutators: {User}}) { + return wrapResponse(null)(User.rejectUsername({id, message})); }, ignoreUser(_, {id}, {mutators: {User}}) { return wrapResponse(null)(User.ignoreUser({id})); diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 824197f3b..0579a2233 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -679,13 +679,21 @@ input SuspendUserInput { # TODO: should this be required? message: String - # If set, the user is requested to change its username. - mustChangeUsername: Boolean - # If set, the suspension lasts at least until specified date. until: Date } +# Input for rejectUsername mutation. +input RejectUsernameInput { + + # id of target user. + id: ID! + + # message to be sent to the user. + # TODO: should this be required? + message: String +} + # DeleteActionResponse is the response returned with possibly some errors # relating to the delete action attempt. type DeleteActionResponse implements Response { @@ -710,6 +718,14 @@ type SuspendUserResponse implements Response { errors: [UserError] } +# RejectUsernameResponse is the response returned with possibly some errors +# relating to the reject username action attempt. +type RejectUsernameResponse implements Response { + + # An array of errors relating to the mutation that occurred. + errors: [UserError] +} + # SetCommentStatusResponse is the response returned with possibly some errors # relating to the delete action attempt. type SetCommentStatusResponse implements Response { @@ -785,6 +801,9 @@ type RootMutation { # Suspends a user. Requires the `ADMIN` role. suspendUser(input: SuspendUserInput!): SuspendUserResponse + # Suspends a user. Requires the `ADMIN` role. + rejectUsername(input: RejectUsernameInput!): RejectUsernameResponse + # Sets Comment status. Requires the `ADMIN` role. setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse diff --git a/models/user.js b/models/user.js index 47c521fba..8d6cbdbb9 100644 --- a/models/user.js +++ b/models/user.js @@ -112,11 +112,7 @@ const UserSchema = new mongoose.Schema({ }, // User's suspension details. - suspensionDetails: { - mustChangeUsername: { - type: Boolean, - default: false, - }, + suspension: { until: { type: Date, default: null, @@ -151,7 +147,6 @@ const UserSchema = new mongoose.Schema({ }, toJSON: { - virtuals: true, transform: function (doc, ret) { delete ret.password; delete ret._id; @@ -160,10 +155,6 @@ const UserSchema = new mongoose.Schema({ } }); -UserSchema.virtual('suspended').get(function() { - return this.suspensionDetails.mustChangeUsername || this.suspensionDetails.until > new Date(); -}); - // Add the indixies on the user profile data. UserSchema.index({ 'profiles.id': 1, @@ -214,6 +205,7 @@ const USER_GRAPH_OPERATIONS = [ 'mutation:editName', 'mutation:setUserStatus', 'mutation:suspendUser', + 'mutation:rejectUsername', 'mutation:setCommentStatus', 'mutation:addCommentTag', 'mutation:removeCommentTag', @@ -233,7 +225,8 @@ UserSchema.method('can', function(...actions) { return false; } - if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:suspendUser' || action === 'mutation:setCommentStatus') && !this.hasRoles('ADMIN')) { + const adminOnlyActions = ['mutation:setUserStatus', 'mutation:suspendUser', 'mutation:rejectUsername', 'mutation:setCommentStatus']; + if (actions.some((action) => adminOnlyActions.indexOf(action) > 0 && !this.hasRoles('ADMIN'))) { return false; } diff --git a/services/users.js b/services/users.js index e4d2982bd..e828307f5 100644 --- a/services/users.js +++ b/services/users.js @@ -450,28 +450,60 @@ module.exports = class UsersService { } /** - * Suspend a user. It changes the status to BANNED and canEditName to True. + * Suspend a user until specified time. * @param {String} id id of a user * @param {String} message message to be send to the user - * @param {Boolean} mustChangeUsername if set the suspension lasts at least until user changed its username. - * @param {Date} until if set the suspension lasts at least until date. + * @param {Date} until date until the suspension is valid. */ - static suspendUser(id, message, mustChangeUsername, until) { - const changes = { - $set: { - suspensionDetails: {}, - } - }; - if (mustChangeUsername) { - changes.$set.status = 'BANNED'; - changes.$set.canEditName = true; - changes.$set.suspensionDetails.mustChangeUsername = true; - } - if (until) { - changes.$set.suspensionDetails.until = until; - } - return UserModel.findOneAndUpdate({id}, changes) + static suspendUser(id, message, until) { + console.log('SUSPEND'); + return UserModel.findOneAndUpdate( + {id}, { + $set: { + suspension: { + until, + }, + } + }) .then((user) => { + console.log(user); + 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: '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 + }; + + return MailerService.sendSimple(options); + } else { + return Promise.reject(errors.ErrMissingEmail); + } + } + }); + } + + /** + * 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 rejectUsername(id, message) { + return UserModel.findOneAndUpdate( + {id}, { + $set: { + status: 'BANNED', + canEditName: true, + } + }).then((user) => { if (message) { let localProfile = user.profiles.find((profile) => profile.provider === 'local'); @@ -822,7 +854,6 @@ module.exports = class UsersService { lowercaseUsername: username.toLowerCase(), canEditName: false, status: 'PENDING', - 'suspensionDetails.mustChangeUsername': false, } }) .then((result) => {