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) => {