added message to ban/suspend workflows

This commit is contained in:
Wyatt Johnson
2017-11-22 14:08:18 -07:00
parent c742101250
commit a537d9ac17
16 changed files with 188 additions and 173 deletions
+6 -6
View File
@@ -17,15 +17,15 @@ const setUserUsernameStatus = async (ctx, id, status) => {
}
};
const setUserBanStatus = async (ctx, id, status) => {
const user = await UsersService.setBanStatus(id, status, ctx.user.id);
const setUserBanStatus = async (ctx, id, status = false, message = null) => {
const user = await UsersService.setBanStatus(id, status, ctx.user.id, message);
if (user.banned) {
ctx.pubsub.publish('userBanned', user);
}
};
const setUserSuspensionStatus = async (ctx, id, until) => {
const user = await UsersService.setSuspensionStatus(id, until, ctx.user.id);
const setUserSuspensionStatus = async (ctx, id, until = null, message = null) => {
const user = await UsersService.setSuspensionStatus(id, until, ctx.user.id, message);
if (user.suspended) {
ctx.pubsub.publish('userSuspended', user);
}
@@ -77,11 +77,11 @@ module.exports = (ctx) => {
}
if (ctx.user.can(SET_USER_BAN_STATUS)) {
mutators.User.setUserBanStatus = (id, status) => setUserBanStatus(ctx, id, status);
mutators.User.setUserBanStatus = (id, status, message) => setUserBanStatus(ctx, id, status, message);
}
if (ctx.user.can(SET_USER_SUSPENSION_STATUS)) {
mutators.User.setUserSuspensionStatus = (id, until) => setUserSuspensionStatus(ctx, id, until);
mutators.User.setUserSuspensionStatus = (id, until, message) => setUserSuspensionStatus(ctx, id, until, message);
}
}
+10 -4
View File
@@ -31,11 +31,17 @@ const RootMutation = {
setUsername: async (_, {id, username}, {mutators: {User}}) => {
await User.setUsername(id, username);
},
setUserSuspensionStatus: async (_, {input: {id, until}}, {mutators: {User}}) => {
await User.setUserSuspensionStatus(id, until);
suspendUser: async (obj, {input: {id, until, message}}, {mutators: {User}}) => {
await User.setUserSuspensionStatus(id, until, message);
},
setUserBanStatus: async (_, {input: {id, status}}, {mutators: {User}}) => {
await User.setUserBanStatus(id, status);
unSuspendUser: async (obj, {input: {id}}, {mutators: {User}}) => {
await User.setUserSuspensionStatus(id);
},
banUser: async (obj, {input: {id, message}}, {mutators: {User}}) => {
await User.setUserBanStatus(id, true, message);
},
unBanUser: async (obj, {input: {id}}, {mutators: {User}}) => {
await User.setUserBanStatus(id, false);
},
ignoreUser: async (_, {id}, {mutators: {User}}) => {
await User.ignoreUser({id});
+41 -47
View File
@@ -1012,15 +1012,6 @@ enum ACTION_ITEM_TYPE {
USERS
}
input CreateLikeInput {
# The item's id for which we are to create a like.
item_id: ID!
# The type of the item for which we are to create the like.
item_type: ACTION_ITEM_TYPE!
}
# CreateCommentInput is the input content used to create a new comment.
input CreateCommentInput {
@@ -1035,7 +1026,6 @@ input CreateCommentInput {
# Tags
tags: [String]
}
input CreateFlagInput {
@@ -1090,16 +1080,6 @@ input CreateDontAgreeInput {
message: String
}
# Input for suspendUser mutation.
input SetUserSuspensionStatusInput {
# id of target user.
id: ID!
# target user will be suspended until this date.
until: Date
}
# Configurable settings that can be overridden for the Asset. You must specify
# all fields that should be updated.
input AssetSettingsInput {
@@ -1158,22 +1138,6 @@ type DeleteActionResponse implements Response {
errors: [UserError!]
}
# SetUserSuspensionStatusResponse is the response returned with possibly some
# errors relating to the suspend action attempt.
type SetUserSuspensionStatusResponse implements Response {
# An array of errors relating to the mutation that occurred.
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 {
@@ -1356,18 +1320,40 @@ type RevokeTokenResponse implements Response {
errors: [UserError!]
}
# SetUserBanStatusInput contains the input to change the ban status of a given
# user.
input SetUserBanStatusInput {
# id is the user to set the ban status on.
input BanUserInput {
id: ID!
# status is the ban status to set on the target user.
status: Boolean!
message: String!
}
type SetUserBanStatusResponse implements Response {
input UnBanUserInput {
id: ID!
}
type BanUsersResponse implements Response {
errors: [UserError!]
}
type UnBanUserResponse implements Response {
errors: [UserError!]
}
input SuspendUserInput {
id: ID!
message: String!
until: Date!
}
type SuspendUserResponse implements Response {
errors: [UserError!]
}
input UnSuspendUserInput {
id: ID!
}
# UnSuspendUserResponse is the response returned with possibly some
# errors relating to the suspend action attempt.
type UnSuspendUserResponse implements Response {
# An array of errors relating to the mutation that occurred.
errors: [UserError!]
@@ -1411,11 +1397,19 @@ type RootMutation {
# Sets the suspension status on a given user. Requires the `MODERATOR` role.
# Mutation is restricted.
setUserSuspensionStatus(input: SetUserSuspensionStatusInput!): SetUserSuspensionStatusResponse
suspendUser(input: SuspendUserInput!): SuspendUserResponse
# Sets the suspension status on a given user. Requires the `MODERATOR` role.
# Mutation is restricted.
unSuspendUser(input: UnSuspendUserInput!): UnSuspendUserResponse
# Sets the ban status on a given user. Requires the `MODERATOR` role.
# Mutation is restricted.
setUserBanStatus(input: SetUserBanStatusInput!): SetUserBanStatusResponse
banUser(input: BanUserInput!): BanUsersResponse
# Sets the ban status on a given user. Requires the `MODERATOR` role.
# Mutation is restricted.
unBanUser(input: UnBanUserInput!): UnBanUserResponse
# Sets the username status on a given user to `APPROVED`. Requires the
# `MODERATOR` role. Mutation is restricted.
+6
View File
@@ -144,6 +144,9 @@ const UserSchema = new Schema({
// assigned_by stores the user id of the user who assigned this status.
assigned_by: {type: String, default: null},
// message stores the email content sent to the user.
message: {type: String, default: null},
// created_at stores the date when this status was assigned.
created_at: {type: Date, default: Date.now}
}],
@@ -166,6 +169,9 @@ const UserSchema = new Schema({
// assigned_by stores the user id of the user who assigned this status.
assigned_by: {type: String, default: null},
// message stores the email content sent to the user.
message: {type: String, default: null},
// created_at stores the date when this status was assigned.
created_at: {type: Date, default: Date.now}
}]
-25
View File
@@ -1,7 +1,6 @@
const express = require('express');
const router = express.Router();
const UsersService = require('../../../services/users');
const mailer = require('../../../services/mailer');
const errors = require('../../../errors');
const authorization = require('../../../middleware/authorization');
const Limit = require('../../../services/limit');
@@ -56,30 +55,6 @@ router.post('/:user_id/role', authorization.needed('ADMIN', 'MODERATOR'), async
}
});
router.post('/:user_id/email', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => {
try {
let user = await UsersService.findById(req.params.user_id);
let localProfile = user.profiles.find((profile) => profile.provider === 'local');
if (!localProfile) {
return next(errors.ErrMissingEmail);
}
await mailer.sendSimple({
template: 'notification', // needed to know which template to render!
locals: { // specifies the template locals.
body: req.body.body
},
subject: req.body.subject,
to: localProfile.id // This only works if the user has registered via e-mail.
});
res.status(204).end();
} catch (e) {
next(e);
}
});
// create a local user.
router.post('/', async (req, res, next) => {
const {email, password, username} = req.body;
+2 -2
View File
@@ -78,7 +78,7 @@ module.exports = class ActionsService {
});
// Emit that there was a new action created.
events.emit(ACTIONS_NEW, foundAction);
await events.emitAsync(ACTIONS_NEW, foundAction);
return foundAction;
}
@@ -233,7 +233,7 @@ module.exports = class ActionsService {
}
// Emit that the action was deleted.
events.emit(ACTIONS_DELETE, action);
await events.emitAsync(ACTIONS_DELETE, action);
return action;
}
+3 -3
View File
@@ -44,7 +44,7 @@ module.exports = class CommentsService {
const savedCommentModel = await commentModel.save();
// Emit that the comment was created!
events.emit(COMMENTS_NEW, savedCommentModel);
await events.emitAsync(COMMENTS_NEW, savedCommentModel);
return savedCommentModel;
}
@@ -193,7 +193,7 @@ module.exports = class CommentsService {
}
}
events.emit(COMMENTS_EDIT, originalComment, editedComment);
await events.emitAsync(COMMENTS_EDIT, originalComment, editedComment);
return editedComment;
}
@@ -305,7 +305,7 @@ module.exports = class CommentsService {
// Emit that the comment was edited, and pass the original comment and the
// edited comment.
events.emit(COMMENTS_EDIT, originalComment, editedComment);
await events.emitAsync(COMMENTS_EDIT, originalComment, editedComment);
return editedComment;
}
-1
View File
@@ -1 +0,0 @@
<%= body %>
-1
View File
@@ -1 +0,0 @@
<%= body %>
-1
View File
@@ -1 +0,0 @@
<%= body.replace(/\n/g, '<br />') %>
-1
View File
@@ -1 +0,0 @@
<%= body %>
+30 -52
View File
@@ -3,7 +3,6 @@ const bcrypt = require('bcryptjs');
const errors = require('../errors');
const some = require('lodash/some');
const merge = require('lodash/merge');
const timeago = require('./timeago');
const {
USERS_NEW,
@@ -33,7 +32,6 @@ const MailerService = require('./mailer');
const i18n = require('./i18n');
const Wordlist = require('./wordlist');
const DomainList = require('./domain_list');
const SettingsService = require('./settings');
const {escapeRegExp} = require('./regex');
const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm';
@@ -90,7 +88,7 @@ class UsersService {
}
}
static async setSuspensionStatus(id, until, assignedBy = null) {
static async setSuspensionStatus(id, until, assignedBy = null, message) {
let user = await UserModel.findOneAndUpdate({id}, {
$set: {
'status.suspension.until': until
@@ -99,11 +97,13 @@ class UsersService {
'status.suspension.history': {
until,
assigned_by: assignedBy,
message,
created_at: Date.now()
}
}
}, {
new: true
new: true,
runValidators: true
});
if (user === null) {
user = await UserModel.findOne({id});
@@ -125,12 +125,12 @@ class UsersService {
}
// Emit that the user username status was changed.
await events.emitAsync(USERS_SUSPENSION_CHANGE, user, until);
await await events.emitAsync(USERS_SUSPENSION_CHANGE, user, {until, message, assignedBy});
return user;
}
static async setBanStatus(id, status, assignedBy = null) {
static async setBanStatus(id, status, assignedBy = null, message) {
let user = await UserModel.findOneAndUpdate({
id,
status: {
@@ -144,6 +144,7 @@ class UsersService {
'status.banned.history': {
status,
assigned_by: assignedBy,
message,
created_at: Date.now()
}
}
@@ -164,7 +165,7 @@ class UsersService {
}
// Emit that the user ban status was changed.
await events.emitAsync(USERS_BAN_CHANGE, user, status);
await await events.emitAsync(USERS_BAN_CHANGE, user, {status, assignedBy, message});
return user;
}
@@ -203,7 +204,7 @@ class UsersService {
}
// Emit that the user username status was changed.
await events.emitAsync(USERS_USERNAME_STATUS_CHANGE, user, status);
await await events.emitAsync(USERS_USERNAME_STATUS_CHANGE, user, {status, assignedBy});
return user;
}
@@ -252,7 +253,7 @@ class UsersService {
}
// Emit that the user username status was changed.
await events.emitAsync(USERS_USERNAME_STATUS_CHANGE, user, toStatus);
await await events.emitAsync(USERS_USERNAME_STATUS_CHANGE, user, toStatus);
return user;
} catch (err) {
@@ -385,7 +386,7 @@ class UsersService {
await user.save();
// Emit that the user was created.
events.emit(USERS_NEW, user);
await events.emitAsync(USERS_NEW, user);
return user;
}
@@ -550,7 +551,7 @@ class UsersService {
}
// Emit that the user was created.
events.emit(USERS_NEW, user);
await events.emitAsync(USERS_NEW, user);
return user;
}
@@ -903,53 +904,30 @@ class UsersService {
module.exports = UsersService;
events.on(USERS_BAN_CHANGE, async (user, status) => {
events.on(USERS_BAN_CHANGE, async (user, {status, message}) => {
// Check to see if the user was banned now and is currently banned.
if (user.banned && status) {
if (user.banned && status && message && message.length > 0) {
await UsersService.sendEmail(user, {
template: 'banned',
locals: {
body: 'In accordance with The Coral Projects community guidelines, your account has been banned. You are now longer allowed to comment, flag or engage with our community.'
},
subject: 'Your account has been banned',
});
}
});
events.on(USERS_SUSPENSION_CHANGE, async (user, until) => {
// Check to see if the user was suspended now and is currently suspended.
if (user.suspended && until !== null && until > Date.now()) {
const {organizationName} = await SettingsService.retrieve();
const message = i18n.t(
'suspenduser.email_message_suspend',
user.username,
organizationName,
timeago(until),
);
await UsersService.sendEmail(user, {
template: 'suspension',
locals: {
body: message,
},
subject: 'Your account has been banned',
});
}
});
events.on(USERS_USERNAME_STATUS_CHANGE, async (user, status) => {
if (status === 'REJECTED') {
const message = i18n.t('reject_username.email_message_reject');
await UsersService.sendEmail(user, {
template: 'suspension',
template: 'plain',
locals: {
body: message
},
subject: 'Username Rejected'
subject: 'Your account has been banned',
});
}
});
events.on(USERS_SUSPENSION_CHANGE, async (user, {until, message}) => {
// Check to see if the user was suspended now and is currently suspended.
if (user.suspended && until !== null && until > Date.now() && message && message.length > 0) {
await UsersService.sendEmail(user, {
template: 'plain',
locals: {
body: message,
},
subject: 'Your account has been suspended',
});
}
});
+45 -16
View File
@@ -5,10 +5,14 @@ const Context = require('../../../../graph/context');
const SettingsService = require('../../../../services/settings');
const UserModel = require('../../../../models/user');
const UsersService = require('../../../../services/users');
const MailerService = require('../../../../services/mailer');
const {expect} = require('chai');
const sinon = require('sinon');
const chai = require('chai');
chai.use(require('sinon-chai'));
const {expect} = chai;
describe('graph.mutations.setUserBanStatus', () => {
describe('graph.mutations.banUser', () => {
let user;
beforeEach(async () => {
await SettingsService.init();
@@ -16,11 +20,34 @@ describe('graph.mutations.setUserBanStatus', () => {
user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA');
});
const setUserBanStatusMutation = `
mutation SetUserBanStatus($user_id: ID!, $status: Boolean!) {
setUserBanStatus(input: {
let spy;
before(() => {
spy = sinon.spy(MailerService, 'sendSimple');
});
afterEach(() => {
spy.reset();
});
after(() => {
spy.restore();
});
const banUserMutation = `
mutation BanUser($user_id: ID!, $message: String!) {
banUser(input: {
id: $user_id,
status: $status
message: $message
}) {
errors {
translation_key
}
}
}
mutation UnBanUser($user_id: ID!) {
unBanUser(input: {
id: $user_id
}) {
errors {
translation_key
@@ -51,40 +78,42 @@ describe('graph.mutations.setUserBanStatus', () => {
const ctx = new Context({user: actor});
const {data, errors} = await graphql(schema, setUserBanStatusMutation, {}, ctx, {
const {data, errors} = await graphql(schema, banUserMutation, {}, ctx, {
user_id: user.id,
status: true
});
message: 'This is a message'
}, 'BanUser');
if (errors && errors.length > 0) {
console.error(errors);
}
expect(errors).to.be.undefined;
if (error) {
expect(data.setUserBanStatus).to.have.property('errors').not.null;
expect(data.setUserBanStatus.errors[0]).to.have.property('translation_key', error);
expect(data.banUser).to.have.property('errors').not.null;
expect(data.banUser.errors[0]).to.have.property('translation_key', error);
} else {
expect(data.setUserBanStatus).to.be.null;
expect(data.banUser).to.be.null;
user = await UserModel.findOne({id: user.id});
expect(user.status.banned.status).to.be.true;
expect(user.status.banned.history).to.have.length(1);
expect(user.status.banned.history[0]).to.have.property('status', true);
expect(user.status.banned.history[0]).to.have.property('message', 'This is a message');
expect(user.status.banned.history[0]).to.have.property('assigned_by', actor.id);
expect(user.status.banned.history[0]).to.have.property('created_at').not.null;
expect(user.banned).to.be.true;
const res = await graphql(schema, setUserBanStatusMutation, {}, ctx, {
expect(spy).to.have.been.calledOnce;
const res = await graphql(schema, banUserMutation, {}, ctx, {
user_id: user.id,
status: false
});
}, 'UnBanUser');
if (res.errors && res.errors.length > 0) {
console.error(res.errors);
}
expect(res.errors).to.be.undefined;
expect(res.data.setUserBanStatus).to.be.null;
expect(res.data.unBanUser).to.be.null;
user = await UserModel.findOne({id: user.id});
@@ -6,12 +6,15 @@ const Context = require('../../../../graph/context');
const SettingsService = require('../../../../services/settings');
const UserModel = require('../../../../models/user');
const UsersService = require('../../../../services/users');
const MailerService = require('../../../../services/mailer');
const sinon = require('sinon');
const chai = require('chai');
chai.use(require('chai-datetime'));
chai.use(require('sinon-chai'));
const {expect} = chai;
describe('graph.mutations.setUserSuspensionStatus', () => {
describe('graph.mutations.suspendUser', () => {
let user;
beforeEach(async () => {
await SettingsService.init();
@@ -19,11 +22,35 @@ describe('graph.mutations.setUserSuspensionStatus', () => {
user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA');
});
const setUserSuspensionStatusMutation = `
mutation SetUserUsernameStatus($user_id: ID!, $until: Date) {
setUserSuspensionStatus(input: {
let spy;
before(() => {
spy = sinon.spy(MailerService, 'sendSimple');
});
afterEach(() => {
spy.reset();
});
after(() => {
spy.restore();
});
const mutation = `
mutation SuspendUser($user_id: ID!, $until: Date!, $message: String!) {
suspendUser(input: {
id: $user_id,
until: $until,
message: $message,
}) {
errors {
translation_key
}
}
}
mutation UnSuspendUser($user_id: ID!) {
unSuspendUser(input: {
id: $user_id,
until: $until
}) {
errors {
translation_key
@@ -57,20 +84,21 @@ describe('graph.mutations.setUserSuspensionStatus', () => {
const now = new Date();
const oneHourFromNow = new Date(new Date(now).setHours(now.getHours() + 1));
const {data, errors} = await graphql(schema, setUserSuspensionStatusMutation, {}, ctx, {
const {data, errors} = await graphql(schema, mutation, {}, ctx, {
user_id: user.id,
until: oneHourFromNow
});
until: oneHourFromNow,
message: 'This is a message'
}, 'SuspendUser');
if (errors && errors.length > 0) {
console.error(errors);
}
expect(errors).to.be.undefined;
if (error) {
expect(data.setUserSuspensionStatus).to.have.property('errors').not.null;
expect(data.setUserSuspensionStatus.errors[0]).to.have.property('translation_key', error);
expect(data.suspendUser).to.have.property('errors').not.null;
expect(data.suspendUser.errors[0]).to.have.property('translation_key', error);
} else {
expect(data.setUserSuspensionStatus).to.be.null;
expect(data.suspendUser).to.be.null;
user = await UserModel.findOne({id: user.id});
@@ -79,6 +107,7 @@ describe('graph.mutations.setUserSuspensionStatus', () => {
expect(user.status.suspension.history).to.have.length(1);
expect(user.status.suspension.history[0]).to.have.property('until').to.be.withinTime(new Date(oneHourFromNow.getTime() - 1000), new Date(oneHourFromNow.getTime() + 1000));
expect(user.status.suspension.history[0]).to.have.property('assigned_by', actor.id);
expect(user.status.suspension.history[0]).to.have.property('message', 'This is a message');
expect(user.status.suspension.history[0]).to.have.property('created_at').not.null;
expect(user.suspended).to.be.true;
@@ -86,15 +115,17 @@ describe('graph.mutations.setUserSuspensionStatus', () => {
expect(user.suspended).to.be.false;
timekeeper.reset();
const res = await graphql(schema, setUserSuspensionStatusMutation, {}, ctx, {
expect(spy).to.have.been.calledOnce;
const res = await graphql(schema, mutation, {}, ctx, {
user_id: user.id,
until: null
});
}, 'UnSuspendUser');
if (res.errors && res.errors.length > 0) {
console.error(res.errors);
}
expect(res.errors).to.be.undefined;
expect(res.data.setUserSuspensionStatus).to.be.null;
expect(res.data.unSuspendUser).to.be.null;
user = await UserModel.findOne({id: user.id});