From 71c743dda7b439423b714a8d367ae8b5b28003c1 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 17 May 2017 19:58:43 +0700 Subject: [PATCH] Extend backend to support new suspend feature --- .../Community/components/SuspendUserDialog.js | 4 +- .../src/graphql/mutations/index.js | 5 +- .../src/graphql/mutations/suspendUser.graphql | 4 +- client/coral-framework/actions/auth.js | 8 ++- .../containers/ChangeUsernameContainer.js | 2 +- graph/mutators/user.js | 4 +- graph/resolvers/comment.js | 4 +- graph/resolvers/root_mutation.js | 4 +- graph/typeDefs.graphql | 23 ++++++- models/user.js | 17 +++++ services/users.js | 68 +++++++++++-------- 11 files changed, 94 insertions(+), 49 deletions(-) diff --git a/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js index 827352c42..0841333be 100644 --- a/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js +++ b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js @@ -52,7 +52,7 @@ class SuspendUserDialog extends Component { const cancel = this.props.handleClose; const next = () => this.setState({stage: stage + 1}); const suspend = () => { - suspendUser({userId: user.user.id, message: this.state.email}) + suspendUser({id: user.user.id, message: this.state.email, mustChangeUsername: true}) .then(() => { this.props.handleClose(); }); @@ -79,7 +79,7 @@ class SuspendUserDialog extends Component { open={open} onClose={handleClose} onCancel={handleClose} - title={lang.t('suspenduser.title')}> + title={lang.t('suspenduser.suspend_user')}>
{lang.t(stages[stage].title, lang.t('suspenduser.username'))}
diff --git a/client/coral-admin/src/graphql/mutations/index.js b/client/coral-admin/src/graphql/mutations/index.js index 16c391907..aad348c5c 100644 --- a/client/coral-admin/src/graphql/mutations/index.js +++ b/client/coral-admin/src/graphql/mutations/index.js @@ -32,11 +32,10 @@ export const setUserStatus = graphql(SET_USER_STATUS, { export const suspendUser = graphql(SUSPEND_USER, { props: ({mutate}) => ({ - suspendUser: ({userId, message}) => { + suspendUser: (input) => { return mutate({ variables: { - userId, - message + input, }, refetchQueries: ['Users'] }); diff --git a/client/coral-admin/src/graphql/mutations/suspendUser.graphql b/client/coral-admin/src/graphql/mutations/suspendUser.graphql index 0d56a3cc0..f6ab2426f 100644 --- a/client/coral-admin/src/graphql/mutations/suspendUser.graphql +++ b/client/coral-admin/src/graphql/mutations/suspendUser.graphql @@ -1,5 +1,5 @@ -mutation suspendUser($userId: ID!, $message: String) { - suspendUser(id: $userId, message: $message) { +mutation suspendUser($input: SuspendUserInput!) { + suspendUser(input: $input) { errors { translation_key } diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 79fba460c..827991cb5 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -187,7 +187,7 @@ export const fetchSignUpFacebook = () => (dispatch) => { ); }; -export const facebookCallback = (err, data) => (dispatch) => { +export const facebookCallback = (err, data) => (dispatch, getState) => { if (err) { dispatch(signInFacebookFailure(err)); return; @@ -196,8 +196,10 @@ export const facebookCallback = (err, data) => (dispatch) => { dispatch(handleAuthToken(data.token)); dispatch(signInFacebookSuccess(data.user)); dispatch(hideSignInDialog()); - dispatch(showCreateUsernameDialog()); - dispatch(hideSignInDialog()); + const {user: {canEditName, suspension}} = getState().auth.toJS(); + if (canEditName && !suspension.mustChangeUsername) { + dispatch(showCreateUsernameDialog()); + } } catch (err) { dispatch(signInFacebookFailure(err)); return; diff --git a/client/coral-sign-in/containers/ChangeUsernameContainer.js b/client/coral-sign-in/containers/ChangeUsernameContainer.js index 12a9a75b8..cebd9605b 100644 --- a/client/coral-sign-in/containers/ChangeUsernameContainer.js +++ b/client/coral-sign-in/containers/ChangeUsernameContainer.js @@ -104,7 +104,7 @@ class ChangeUsernameContainer extends Component { return (
{ return UsersService.setStatus(id, status); }; -const suspendUser = ({user}, {id, message}) => { - return UsersService.suspendUser(id, message); +const suspendUser = ({user}, {id, message, mustChangeUsername, until}) => { + return UsersService.suspendUser(id, message, mustChangeUsername, until); }; const ignoreUser = ({user}, userToIgnore) => { diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 8d0473900..8074abe67 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -25,9 +25,9 @@ const Comment = { // TODO: remove if (user && excludeIgnored) { - return Comments.countByParentIDPersonalized({id, excludeIgnored}); + return Comments.countByParentIDPersonalized({id, excludeIgnored}); } - return Comments.countByParentID.load(id); + return Comments.countByParentID.load(id); }, actions({id}, _, {user, loaders: {Actions}}) { diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 2abc88f0d..0b954f2a1 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -20,8 +20,8 @@ const RootMutation = { setUserStatus(_, {id, status}, {mutators: {User}}) { return wrapResponse(null)(User.setUserStatus({id, status})); }, - suspendUser(_, {id, message}, {mutators: {User}}) { - return wrapResponse(null)(User.suspendUser({id, message})); + suspendUser(_, {input: {id, message, mustChangeUsername, until}}, {mutators: {User}}) { + return wrapResponse(null)(User.suspendUser({id, message, mustChangeUsername, until})); }, ignoreUser(_, {id}, {mutators: {User}}) { return wrapResponse(null)(User.ignoreUser({id})); diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index f51258eb1..824197f3b 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -669,6 +669,23 @@ input CreateDontAgreeInput { message: String } +# Input for suspendUser mutation. +input SuspendUserInput { + + # id of target user. + id: ID! + + # message to be sent to the user. + # 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 +} + # DeleteActionResponse is the response returned with possibly some errors # relating to the delete action attempt. type DeleteActionResponse implements Response { @@ -741,7 +758,7 @@ type EditCommentResponse implements Response { comment: Comment # An array of errors relating to the mutation that occured. - errors: [UserError] + errors: [UserError] } # All mutations for the application are defined on this object. @@ -765,8 +782,8 @@ type RootMutation { # Sets User status. Requires the `ADMIN` role. setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse - # Sets User status to BANNED and canEditName to true. It sends a message to the banned User. Requires the `ADMIN` role. - suspendUser(id: ID!, message: String): SuspendUserResponse + # Suspends a user. Requires the `ADMIN` role. + suspendUser(input: SuspendUserInput!): SuspendUserResponse # 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 56f750d34..47c521fba 100644 --- a/models/user.js +++ b/models/user.js @@ -111,6 +111,18 @@ const UserSchema = new mongoose.Schema({ default: false }, + // User's suspension details. + suspensionDetails: { + mustChangeUsername: { + type: Boolean, + default: false, + }, + until: { + type: Date, + default: null, + }, + }, + // User's settings settings: { bio: { @@ -139,6 +151,7 @@ const UserSchema = new mongoose.Schema({ }, toJSON: { + virtuals: true, transform: function (doc, ret) { delete ret.password; delete ret._id; @@ -147,6 +160,10 @@ 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, diff --git a/services/users.js b/services/users.js index 9d15e129f..e4d2982bd 100644 --- a/services/users.js +++ b/services/users.js @@ -451,40 +451,48 @@ module.exports = class UsersService { /** * Suspend a user. It changes the status to BANNED and canEditName to True. - * @param {String} id id of a user - * @param {Function} done callback after the operation is complete + * @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. */ - static suspendUser(id, message) { - return UserModel.findOneAndUpdate({ - id - }, { + static suspendUser(id, message, mustChangeUsername, until) { + const changes = { $set: { - status: 'BANNED', - canEditName: true + suspensionDetails: {}, } - }) - .then((user) => { - if (message) { - let localProfile = user.profiles.find((profile) => profile.provider === 'local'); + }; + 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) + .then((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 - }; + 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); + return MailerService.sendSimple(options); + } else { + return Promise.reject(errors.ErrMissingEmail); + } } - } - }); + }); } /** @@ -813,13 +821,15 @@ module.exports = class UsersService { username: username, lowercaseUsername: username.toLowerCase(), canEditName: false, - status: 'PENDING' + status: 'PENDING', + 'suspensionDetails.mustChangeUsername': false, } }) .then((result) => { if (result.nModified <= 0) { return Promise.reject(errors.ErrPermissionUpdateUsername); } + console.log(result); return result; })