Merge pull request #717 from coralproject/live-suspension

Live update on stream when banned user, suspended user or rejected username
This commit is contained in:
Kim Gardner
2017-06-28 10:16:18 +01:00
committed by GitHub
32 changed files with 402 additions and 132 deletions
@@ -195,8 +195,16 @@ export default class Comment extends React.Component {
editComment: React.PropTypes.func,
}
editComment = (...args) => {
return this.props.editComment(this.props.comment.id, this.props.asset.id, ...args);
}
onClickEdit (e) {
e.preventDefault();
if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
this.props.addNotification('error', t('error.NOT_AUTHORIZED'));
return;
}
this.setState({isEditing: true});
}
@@ -239,7 +247,8 @@ export default class Comment extends React.Component {
}
if (can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
this.props.setActiveReplyBox(this.props.comment.id);
return;
} else {
this.props.addNotification('error', t('error.NOT_AUTHORIZED'));
}
return;
}
@@ -468,7 +477,7 @@ export default class Comment extends React.Component {
{
this.state.isEditing
? <EditableCommentContent
editComment={this.props.editComment.bind(null, comment.id, asset.id)}
editComment={this.editComment}
addNotification={addNotification}
asset={asset}
comment={comment}
@@ -527,6 +536,7 @@ export default class Comment extends React.Component {
id={comment.id}
author_id={comment.user.id}
postFlag={postFlag}
addNotification={addNotification}
postDontAgree={postDontAgree}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
@@ -545,8 +555,8 @@ export default class Comment extends React.Component {
setActiveReplyBox={setActiveReplyBox}
parentId={parentId || comment.id}
addNotification={addNotification}
authorId={currentUser.id}
postComment={postComment}
currentUser={currentUser}
assetId={asset.id}
/>
: null}
@@ -4,6 +4,7 @@ import {CommentForm} from 'coral-plugin-commentbox/CommentForm';
import styles from './Comment.css';
import {CountdownSeconds} from './CountdownSeconds';
import {getEditableUntilDate} from './util';
import {can} from 'coral-framework/services/perms';
import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
@@ -47,7 +48,6 @@ export class EditableCommentContent extends React.Component {
}
constructor(props) {
super(props);
this.editComment = this.editComment.bind(this);
this.editWindowExpiryTimeout = null;
}
componentDidMount() {
@@ -65,7 +65,12 @@ export class EditableCommentContent extends React.Component {
this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
}
}
async editComment(edit) {
editComment = async (edit) => {
if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
this.props.addNotification('error', t('error.NOT_AUTHORIZED'));
return;
}
const {editComment, addNotification, stopEditing} = this.props;
if (typeof editComment !== 'function') {return;}
let response;
@@ -57,7 +57,10 @@ class Stream extends React.Component {
constructor(props) {
super(props);
this.state = resetCursors(this.state, props);
this.state = {
...resetCursors(this.state, props),
keepCommentBox: false,
};
}
componentWillReceiveProps(next) {
@@ -68,6 +71,12 @@ class Stream extends React.Component {
this.setState(resetCursors);
return;
}
// Keep comment box when user was live suspended, banned, ...
if (!this.userIsDegraged(this.props) && this.userIsDegraged(next)) {
this.setState({keepCommentBox: true});
}
if (
prevComments && nextComments &&
nextComments.nodes.length < prevComments.nodes.length
@@ -133,6 +142,10 @@ class Stream extends React.Component {
return view;
}
userIsDegraged({auth: {user}} = this.props) {
return !can(user, 'INTERACT_WITH_COMMUNITY');
}
render() {
const {
commentClassNames,
@@ -150,6 +163,7 @@ class Stream extends React.Component {
pluginProps,
editName
} = this.props;
const {keepCommentBox} = this.state;
const view = this.getVisibleComments();
const open = asset.closedAt === null;
@@ -171,6 +185,8 @@ class Stream extends React.Component {
me.ignoredUsers.find((u) => u.id === comment.user.id)
);
};
const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox);
return (
<div id="stream">
@@ -199,10 +215,7 @@ class Stream extends React.Component {
editName={editName}
currentUsername={user.username}
/>}
{loggedIn &&
!banned &&
!temporarilySuspended &&
!highlightedComment &&
{showCommentBox &&
<CommentBox
addNotification={this.props.addNotification}
postComment={this.props.postComment}
@@ -211,7 +224,7 @@ class Stream extends React.Component {
assetId={asset.id}
premod={asset.settings.moderation}
isReply={false}
authorId={user.id}
currentUser={user}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
/>}
@@ -12,6 +12,8 @@ import {getDefinitionName} from 'coral-framework/utils';
import {withQuery} from 'coral-framework/hocs';
import Embed from '../components/Embed';
import Stream from './Stream';
import {addNotification} from 'coral-framework/actions/notification';
import t from 'coral-framework/services/i18n';
import {setActiveTab} from '../actions/embed';
import {viewAllComments} from '../actions/stream';
@@ -20,12 +22,63 @@ const {logout, checkLogin} = authActions;
const {fetchAssetSuccess} = assetActions;
class EmbedContainer extends React.Component {
subscriptions = [];
subscribeToUpdates(props = this.props) {
if (props.auth.loggedIn) {
const newSubscriptions = [{
document: USER_BANNED_SUBSCRIPTION,
updateQuery: () => {
addNotification('info', t('your_account_has_been_banned'));
},
},
{
document: USER_SUSPENDED_SUBSCRIPTION,
updateQuery: () => {
addNotification('info', t('your_account_has_been_suspended'));
},
},
{
document: USERNAME_REJECTED_SUBSCRIPTION,
updateQuery: () => {
addNotification('info', t('your_username_has_been_rejected'));
},
}];
this.subscriptions = newSubscriptions.map((s) => props.data.subscribeToMore({
document: s.document,
variables: {
user_id: props.auth.user.id,
},
updateQuery: s.updateQuery,
}));
}
}
unsubscribe() {
this.subscriptions.forEach((unsubscribe) => unsubscribe());
this.subscriptions = [];
}
resubscribe(props) {
this.unsubscribe();
this.subscribeToUpdates(props);
}
componentDidMount() {
this.subscribeToUpdates();
}
componentWillUnmount() {
this.unsubscribe();
}
componentWillReceiveProps(nextProps) {
if (this.props.auth.loggedIn !== nextProps.auth.loggedIn) {
// Refetch after login/logout.
this.props.data.refetch();
this.resubscribe(nextProps);
}
const {fetchAssetSuccess} = this.props;
@@ -52,6 +105,45 @@ class EmbedContainer extends React.Component {
}
}
const USER_BANNED_SUBSCRIPTION = gql`
subscription UserBanned($user_id: ID!) {
userBanned(user_id: $user_id){
id
status
canEditName
suspension {
until
}
}
}
`;
const USER_SUSPENDED_SUBSCRIPTION = gql`
subscription UserSuspended($user_id: ID!) {
userSuspended(user_id: $user_id){
id
status
canEditName
suspension {
until
}
}
}
`;
const USERNAME_REJECTED_SUBSCRIPTION = gql`
subscription UsernameRejected($user_id: ID!) {
usernameRejected(user_id: $user_id){
id
status
canEditName
suspension {
until
}
}
}
`;
const EMBED_QUERY = gql`
query CoralEmbedStream_Embed($assetId: ID, $assetUrl: String, $commentId: ID!, $hasComment: Boolean!, $excludeIgnored: Boolean) {
asset(id: $assetId, url: $assetUrl) {
@@ -95,6 +187,7 @@ const mapDispatchToProps = (dispatch) =>
setActiveTab,
viewAllComments,
fetchAssetSuccess,
addNotification,
},
dispatch
);
@@ -30,11 +30,8 @@ class StreamContainer extends React.Component {
subscriptions = [];
subscribeToUpdates() {
const sub1 = this.props.data.subscribeToMore({
const newSubscriptions = [{
document: COMMENTS_EDITED_SUBSCRIPTION,
variables: {
assetId: this.props.root.asset.id,
},
updateQuery: (prev, {subscriptionData: {data: {commentEdited}}}) => {
// Ignore mutations from me.
@@ -52,13 +49,9 @@ class StreamContainer extends React.Component {
return removeCommentFromEmbedQuery(prev, commentEdited.id);
}
},
});
const sub2 = this.props.data.subscribeToMore({
},
{
document: COMMENTS_ADDED_SUBSCRIPTION,
variables: {
assetId: this.props.root.asset.id,
},
updateQuery: (prev, {subscriptionData: {data: {commentAdded}}}) => {
// Ignore mutations from me.
@@ -81,9 +74,15 @@ class StreamContainer extends React.Component {
return insertCommentIntoEmbedQuery(prev, commentAdded);
}
});
}];
this.subscriptions.push(sub1, sub2);
this.subscriptions = newSubscriptions.map((s) => this.props.data.subscribeToMore({
document: s.document,
variables: {
assetId: this.props.root.asset.id,
},
updateQuery: s.updateQuery,
}));
}
unsubscribe() {
@@ -173,7 +172,7 @@ const commentFragment = gql`
`;
const COMMENTS_ADDED_SUBSCRIPTION = gql`
subscription onCommentAdded($assetId: ID!, $excludeIgnored: Boolean){
subscription CommentAdded($assetId: ID!, $excludeIgnored: Boolean){
commentAdded(asset_id: $assetId){
parent {
id
@@ -185,7 +184,7 @@ const COMMENTS_ADDED_SUBSCRIPTION = gql`
`;
const COMMENTS_EDITED_SUBSCRIPTION = gql`
subscription onCommentEdited($assetId: ID!){
subscription CommentEdited($assetId: ID!){
commentEdited(asset_id: $assetId){
id
body
+7 -5
View File
@@ -28,6 +28,7 @@ const snackbarStyles = {
// This function should return value of window.Coral
const Coral = {};
const Talk = (Coral.Talk = {});
let notificationTimeout = null;
// build the URL to load in the pym iframe
function buildStreamIframeUrl(talkBaseUrl, query) {
@@ -110,14 +111,15 @@ function configurePymParent(pymParent, opts) {
snackbar.className = `coral-notif-${type}`;
snackbar.textContent = text;
setTimeout(() => {
clearTimeout(notificationTimeout);
notificationTimeout = setTimeout(() => {
snackbar.style.transform = 'translate(-50%, 0)';
snackbar.style.opacity = 1;
}, 0);
setTimeout(() => {
snackbar.style.opacity = 0;
}, 5000);
notificationTimeout = setTimeout(() => {
snackbar.style.opacity = 0;
}, 7000);
}, 0);
});
// Helps child show notifications at the right scrollTop
+14
View File
@@ -151,6 +151,20 @@ export default function auth (state = initialState, action) {
case actions.SET_REDIRECT_URI:
return state
.set('redirectUri', action.uri);
case 'APOLLO_SUBSCRIPTION_RESULT':
if (action.operationName === 'UserBanned' && state.getIn(['user', 'id']) === action.variables.user_id) {
return state
.mergeIn(['user'], action.result.data.userBanned);
}
if (action.operationName === 'UserSuspended' && state.getIn(['user', 'id']) === action.variables.user_id) {
return state
.mergeIn(['user'], action.result.data.userSuspended);
}
if (action.operationName === 'UsernameRejected' && state.getIn(['user', 'id']) === action.variables.user_id) {
return state
.mergeIn(['user'], action.result.data.usernameRejected);
}
return state;
default :
return state;
}
+10 -3
View File
@@ -1,6 +1,7 @@
import React, {PropTypes} from 'react';
import t from 'coral-framework/services/i18n';
import {can} from 'coral-framework/services/perms';
import Slot from 'coral-framework/components/Slot';
import {connect} from 'react-redux';
@@ -43,8 +44,14 @@ class CommentBox extends React.Component {
assetId,
parentId,
addNotification,
currentUser,
} = this.props;
if (!can(currentUser, 'INTERACT_WITH_COMMUNITY')) {
addNotification('error', t('error.NOT_AUTHORIZED'));
return;
}
let comment = {
asset_id: assetId,
parent_id: parentId,
@@ -126,7 +133,7 @@ class CommentBox extends React.Component {
handleChange = (e) => this.setState({body: e.target.value});
render () {
const {styles, isReply, authorId, maxCharCount} = this.props;
const {styles, isReply, currentUser, maxCharCount} = this.props;
let {cancelButtonClicked} = this.props;
if (isReply && typeof cancelButtonClicked !== 'function') {
@@ -145,7 +152,7 @@ class CommentBox extends React.Component {
charCountEnable={this.props.charCountEnable}
bodyPlaceholder={t('comment.comment')}
bodyInputId={isReply ? 'replyText' : 'commentText'}
saveComment={authorId && this.postComment}
saveComment={currentUser && this.postComment}
buttonContainerStart={<Slot
fill="commentInputDetailArea"
registerHook={this.registerHook}
@@ -170,7 +177,7 @@ CommentBox.propTypes = {
cancelButtonClicked: PropTypes.func,
assetId: PropTypes.string.isRequired,
parentId: PropTypes.string,
authorId: PropTypes.string.isRequired,
currentUser: PropTypes.object.isRequired,
isReply: PropTypes.bool.isRequired,
canPost: PropTypes.bool,
};
+2
View File
@@ -39,6 +39,8 @@ export default class FlagButton extends Component {
} else {
this.setState({showMenu: true});
}
} else {
this.props.addNotification('error', t('error.NOT_AUTHORIZED'));
}
}
+2 -2
View File
@@ -18,7 +18,7 @@ class ReplyBox extends Component {
styles,
postComment,
assetId,
authorId,
currentUser,
addNotification,
parentId,
commentPostedHandler,
@@ -33,7 +33,7 @@ class ReplyBox extends Component {
parentId={parentId}
cancelButtonClicked={this.cancelReply}
addNotification={addNotification}
authorId={authorId}
currentUser={currentUser}
assetId={assetId}
postComment={postComment}
isReply={true} />
+2 -1
View File
@@ -3,6 +3,7 @@ const mutators = require('./mutators');
const uuid = require('uuid');
const plugins = require('../services/plugins');
const pubsub = require('../services/pubsub');
const debug = require('debug')('talk:graph:context');
/**
@@ -32,7 +33,7 @@ const decorateContextPlugins = (context, contextPlugins) => contextPlugins.reduc
* Stores the request context.
*/
class Context {
constructor({user = null}, pubsub) {
constructor({user = null}) {
// Generate a new context id for the request.
this.id = uuid.v4();
+1 -2
View File
@@ -1,6 +1,5 @@
const schema = require('./schema');
const Context = require('./context');
const pubsub = require('./pubsub');
const {createSubscriptionManager} = require('./subscriptions');
module.exports = {
@@ -11,7 +10,7 @@ module.exports = {
// Load in the new context here, this'll create the loaders + mutators for
// the lifespan of this request.
context: new Context(req, pubsub)
context: new Context(req)
}),
createSubscriptionManager
};
+2 -2
View File
@@ -16,7 +16,7 @@ const {CREATE_ACTION, DELETE_ACTION} = require('../../perms/constants');
const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id, item_type, action_type, group_id, metadata = {}}) => {
let comment;
if (pubsub && item_type === 'COMMENTS') {
if (item_type === 'COMMENTS') {
comment = await Comments.get.load(item_id);
if (!comment) {
throw new Error('Comment not found');
@@ -38,7 +38,7 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id,
await UsersService.setStatus(item_id, 'PENDING');
}
if (pubsub && comment) {
if (comment) {
pubsub.publish('commentFlagged', comment);
}
+10 -18
View File
@@ -177,11 +177,8 @@ const createComment = async (context, {tags = [], body, asset_id, parent_id = nu
}
Comments.countByAssetID.incr(asset_id);
if (pubsub) {
// Publish the newly added comment via the subscription.
pubsub.publish('commentAdded', comment);
}
// Publish the newly added comment via the subscription.
pubsub.publish('commentAdded', comment);
}
return comment;
@@ -346,17 +343,14 @@ const setStatus = async ({user, loaders: {Comments}, pubsub}, {id, status}) => {
// adjust the affected user's karma in the next tick.
process.nextTick(adjustKarma(Comments, id, status));
if (pubsub) {
if (status === 'ACCEPTED') {
if (status === 'ACCEPTED') {
// Publish the comment status change via the subscription.
pubsub.publish('commentAccepted', comment);
} else if (status === 'REJECTED') {
// Publish the comment status change via the subscription.
pubsub.publish('commentAccepted', comment);
} else if (status === 'REJECTED') {
// Publish the comment status change via the subscription.
pubsub.publish('commentRejected', comment);
}
// Publish the comment status change via the subscription.
pubsub.publish('commentRejected', comment);
}
return comment;
@@ -379,11 +373,9 @@ const edit = async (context, {id, asset_id, edit: {body}}) => {
// Execute the edit.
const comment = await CommentsService.edit(id, context.user.id, {body, status});
if (context.pubsub) {
// Publish the edited comment via the subscription.
context.pubsub.publish('commentEdited', comment);
// Publish the edited comment via the subscription.
context.pubsub.publish('commentEdited', comment);
}
return comment;
};
+18 -6
View File
@@ -2,16 +2,28 @@ const errors = require('../../errors');
const UsersService = require('../../services/users');
const {SET_USER_STATUS, SUSPEND_USER, REJECT_USERNAME} = require('../../perms/constants');
const setUserStatus = ({user}, {id, status}) => {
return UsersService.setStatus(id, status);
const setUserStatus = async ({user, pubsub}, {id, status}) => {
const result = await UsersService.setStatus(id, status);
if (result && result.status === 'BANNED') {
pubsub.publish('userBanned', result);
}
return result;
};
const suspendUser = ({user}, {id, message, until}) => {
return UsersService.suspendUser(id, message, until);
const suspendUser = async ({user, pubsub}, {id, message, until}) => {
const result = await UsersService.suspendUser(id, message, until);
if (result) {
pubsub.publish('userSuspended', result);
}
return result;
};
const rejectUsername = ({user}, {id, message}) => {
return UsersService.rejectUsername(id, message);
const rejectUsername = async ({user, pubsub}, {id, message}) => {
const result = await UsersService.rejectUsername(id, message);
if (result) {
pubsub.publish('usernameRejected', result);
}
return result;
};
const ignoreUser = ({user}, userToIgnore) => {
+9
View File
@@ -14,6 +14,15 @@ const Subscription = {
commentFlagged(comment) {
return comment;
},
userBanned(user) {
return user;
},
userSuspended(user) {
return user;
},
usernameRejected(user) {
return user;
},
};
module.exports = Subscription;
+8
View File
@@ -6,6 +6,7 @@ const {
SEARCH_OTHERS_COMMENTS,
UPDATE_USER_ROLES,
SEARCH_COMMENT_METRICS,
VIEW_SUSPENSION_INFO,
LIST_OWN_TOKENS
} = require('../../perms/constants');
@@ -84,6 +85,13 @@ const User = {
if (requestingUser && requestingUser.can(SEARCH_COMMENT_METRICS)) {
return KarmaService.model(user);
}
},
suspension({id, suspension}, _, {user}) {
if (user.id !== id && !user.can(VIEW_SUSPENSION_INFO)) {
return null;
}
return suspension;
}
};
+45 -3
View File
@@ -3,7 +3,7 @@ const {SubscriptionServer} = require('subscriptions-transport-ws');
const _ = require('lodash');
const debug = require('debug')('talk:graph:subscriptions');
const pubsub = require('./pubsub');
const pubsub = require('../services/pubsub');
const schema = require('./schema');
const Context = require('./context');
const plugins = require('../services/plugins');
@@ -21,6 +21,9 @@ const {
SUBSCRIBE_COMMENT_FLAGGED,
SUBSCRIBE_ALL_COMMENT_EDITED,
SUBSCRIBE_ALL_COMMENT_ADDED,
SUBSCRIBE_ALL_USER_SUSPENDED,
SUBSCRIBE_ALL_USER_BANNED,
SUBSCRIBE_ALL_USERNAME_REJECTED,
} = require('../perms/constants');
/**
@@ -83,6 +86,45 @@ const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plu
}
},
}),
userSuspended: (options, args) => ({
userSuspended: {
filter: (user, context) => {
if (
!context.user
|| args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_SUSPENDED)
) {
return false;
}
return !args.user_id || user.id === args.user_id;
}
},
}),
userBanned: (options, args) => ({
userBanned: {
filter: (user, context) => {
if (
!context.user
|| args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_BANNED)
) {
return false;
}
return !args.user_id || user.id === args.user_id;
}
},
}),
usernameRejected: (options, args) => ({
usernameRejected: {
filter: (user, context) => {
if (
!context.user
|| args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USERNAME_REJECTED)
) {
return false;
}
return !args.user_id || user.id === args.user_id;
}
},
}),
});
/**
@@ -117,10 +159,10 @@ const createSubscriptionManager = (server) => new SubscriptionServer({
} catch (e) {
console.error(e);
return new Context({}, pubsub);
return new Context({});
}
return new Context(req, pubsub);
return new Context(req);
};
return baseParams;
+23
View File
@@ -61,6 +61,10 @@ type UserProfile {
provider: String!
}
type SuspensionInfo {
until: Date
}
# Any person who can author comments, create actions, and view comments on a
# stream.
type User {
@@ -108,6 +112,10 @@ type User {
# returns user status
status: USER_STATUS
# returns suspension info. Only available to Admins and Moderators
# or on own logged in User.
suspension: SuspensionInfo
}
# UsersQuery allows the ability to query users by a specific fields.
@@ -1034,6 +1042,21 @@ type Subscription {
# Get an update whenever a comment has been rejected.
# Requires the `ADMIN` or `MODERATOR` role.
commentRejected(asset_id: ID): Comment
# Get an update whenever a user has been suspended.
# `user_id` must match id of current user except for
# users with the `ADMIN` or `MODERATOR` role.
userSuspended(user_id: ID): User
# Get an update whenever a user has been banned.
# `user_id` must match id of current user except for
# users with the `ADMIN` or `MODERATOR` role.
userBanned(user_id: ID): User
# Get an update whenever a username has been rejected.
# `user_id` must match id of current user except for
# users with the `ADMIN` or `MODERATOR` role.
usernameRejected(user_id: ID): User
}
################################################################################
+3
View File
@@ -1,4 +1,7 @@
en:
your_account_has_been_suspended: Your account has been temporarily suspended.
your_account_has_been_banned: Your account has been banned.
your_username_has_been_rejected: Your account has been suspended because your username has been deemed inappropriate. To restore your account please enter a new username.
bandialog:
are_you_sure: "Are you sure you would like to ban {0}?"
ban_user: "Ban User?"
+3
View File
@@ -1,4 +1,7 @@
es:
your_account_has_been_suspended: Su cuenta ha sido temporalmente suspendida.
your_account_has_been_banned: Su cuenta ha sido suspendida.
your_username_has_been_rejected: Su cuenta ha sido suspendida porque tu nombre de usuario ha sido considerado no apropiado para el espacio. Para recuperar la cuenta, por favor ingresar un nuevo nombre de usuario.
bandialog:
are_you_sure: "¿Estás segura que quieres suspender a {0}?"
ban_user: "¿Quieres suspender el Usuario?"
+4
View File
@@ -26,6 +26,7 @@ module.exports = {
SEARCH_COMMENT_METRICS: 'SEARCH_COMMENT_METRICS',
LIST_OWN_TOKENS: 'LIST_OWN_TOKENS',
SEARCH_COMMENT_STATUS_HISTORY: 'SEARCH_COMMENT_STATUS_HISTORY',
VIEW_SUSPENSION_INFO: 'VIEW_SUSPENSION_INFO',
// subscriptions
SUBSCRIBE_COMMENT_ACCEPTED: 'SUBSCRIBE_COMMENT_ACCEPTED',
@@ -33,4 +34,7 @@ module.exports = {
SUBSCRIBE_COMMENT_FLAGGED: 'SUBSCRIBE_COMMENT_FLAGGED',
SUBSCRIBE_ALL_COMMENT_ADDED: 'SUBSCRIBE_ALL_COMMENT_ADDED',
SUBSCRIBE_ALL_COMMENT_EDITED: 'SUBSCRIBE_ALL_COMMENT_EDITED',
SUBSCRIBE_ALL_USER_SUSPENDED: 'SUBSCRIBE_ALL_USER_SUSPENDED',
SUBSCRIBE_ALL_USER_BANNED: 'SUBSCRIBE_ALL_USER_BANNED',
SUBSCRIBE_ALL_USERNAME_REJECTED: 'SUBSCRIBE_ALL_USERNAME_REJECTED',
};
+2
View File
@@ -19,6 +19,8 @@ module.exports = (user, perm) => {
return check(user, ['ADMIN']);
case types.SEARCH_COMMENT_STATUS_HISTORY:
return check(user, ['ADMIN', 'MODERATOR']);
case types.VIEW_SUSPENSION_INFO:
return check(user, ['ADMIN', 'MODERATOR']);
default:
break;
}
+6
View File
@@ -13,6 +13,12 @@ module.exports = (user, perm) => {
return check(user, ['ADMIN', 'MODERATOR']);
case types.SUBSCRIBE_ALL_COMMENT_ADDED:
return check(user, ['ADMIN', 'MODERATOR']);
case types.SUBSCRIBE_ALL_USER_SUSPENDED:
return check(user, ['ADMIN', 'MODERATOR']);
case types.SUBSCRIBE_ALL_USER_BANNED:
return check(user, ['ADMIN', 'MODERATOR']);
case types.SUBSCRIBE_ALL_USERNAME_REJECTED:
return check(user, ['ADMIN', 'MODERATOR']);
default:
break;
}
+3 -1
View File
@@ -8,6 +8,7 @@ import {compose, gql} from 'react-apollo';
import withFragments from 'coral-framework/hocs/withFragments';
import withMutation from 'coral-framework/hocs/withMutation';
import {showSignInDialog} from 'coral-framework/actions/auth';
import {addNotification} from 'coral-framework/actions/notification';
import {capitalize} from 'coral-framework/helpers/strings';
import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
import * as PropTypes from 'prop-types';
@@ -248,6 +249,7 @@ export default (reaction) => (WrappedComponent) => {
return <WrappedComponent
showSignInDialog={this.props.showSignInDialog}
addNotification={this.props.addNotification}
user={this.props.user}
comment={comment}
reactionSummary={reactionSummary}
@@ -350,7 +352,7 @@ export default (reaction) => (WrappedComponent) => {
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({showSignInDialog}, dispatch);
bindActionCreators({showSignInDialog, addNotification}, dispatch);
const enhance = compose(
withFragments({
@@ -13,6 +13,7 @@ class LikeButton extends React.Component {
postReaction,
deleteReaction,
showSignInDialog,
addNotification,
alreadyReacted,
user,
} = this.props;
@@ -25,6 +26,7 @@ class LikeButton extends React.Component {
// If the current user is suspended, do nothing.
if (!can(user, 'INTERACT_WITH_COMMUNITY')) {
addNotification('error', t('error.NOT_AUTHORIZED'));
return;
}
@@ -13,6 +13,7 @@ class LoveButton extends React.Component {
postReaction,
deleteReaction,
showSignInDialog,
addNotification,
alreadyReacted,
user,
} = this.props;
@@ -25,6 +26,7 @@ class LoveButton extends React.Component {
// If the current user is suspended, do nothing.
if (!can(user, 'INTERACT_WITH_COMMUNITY')) {
addNotification('error', t('error.NOT_AUTHORIZED'));
return;
}
@@ -14,6 +14,7 @@ class RespectButton extends React.Component {
deleteReaction,
showSignInDialog,
alreadyReacted,
addNotification,
user,
} = this.props;
@@ -25,6 +26,7 @@ class RespectButton extends React.Component {
// If the current user is suspended, do nothing.
if (!can(user, 'INTERACT_WITH_COMMUNITY')) {
addNotification('error', t('error.NOT_AUTHORIZED'));
return;
}
+10 -5
View File
@@ -1,8 +1,8 @@
const express = require('express');
const router = express.Router();
const UsersService = require('../../../services/users');
const CommentsService = require('../../../services/comments');
const mailer = require('../../../services/mailer');
const pubsub = require('../../../services/pubsub');
const errors = require('../../../errors');
const authorization = require('../../../middleware/authorization');
const i18n = require('../../../services/i18n');
@@ -53,11 +53,16 @@ router.post('/:user_id/role', authorization.needed('ADMIN'), (req, res, next) =>
router.post('/:user_id/status', authorization.needed('ADMIN'), (req, res, next) => {
UsersService
.setStatus(req.params.user_id, req.body.status)
.then((status) => {
res.status(201).json(status);
.then((user) => {
if (status === 'BANNED' && req.body.comment_id) {
return CommentsService.pushStatus(req.body.comment_id, 'rejected', req.params.user_id);
// TODO: current updating status behavior is weird.
if (user) {
if (user.status === 'BANNED') {
pubsub.publish('userBanned', user);
}
res.status(201).json(user.status);
} else {
res.status(500).json();
}
})
.catch(next);
+1 -1
View File
@@ -1,5 +1,5 @@
const {RedisPubSub} = require('graphql-redis-subscriptions');
const {connectionOptions} = require('../services/redis');
const {connectionOptions} = require('./redis');
module.exports = new RedisPubSub({connection: connectionOptions});
+54 -43
View File
@@ -435,7 +435,10 @@ module.exports = class UsersService {
return Promise.reject(new Error(`status ${status} is not supported`));
}
return UserModel.update({
// TODO: current updating status behavior is weird.
// once a user has been `APPROVED` its status cannot be
// changed anymore.
return UserModel.findOneAndUpdate({
id,
status: {
$ne: 'APPROVED'
@@ -444,6 +447,8 @@ module.exports = class UsersService {
$set: {
status
}
}, {
new: true,
});
}
@@ -453,34 +458,37 @@ module.exports = class UsersService {
* @param {String} message message to be send to the user
* @param {Date} until date until the suspension is valid.
*/
static suspendUser(id, message, until) {
return UserModel.findOneAndUpdate(
static async suspendUser(id, message, until) {
const user = await UserModel.findOneAndUpdate(
{id}, {
$set: {
suspension: {
until,
},
}
})
.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: 'Your account has been suspended',
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);
}
}
}, {
new: true,
});
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: 'Your account has been suspended',
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
};
await MailerService.sendSimple(options);
}
}
return user;
}
/**
@@ -489,34 +497,37 @@ module.exports = class UsersService {
* @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({
static async rejectUsername(id, message) {
const user = await UserModel.findOneAndUpdate({
id
}, {
$set: {
status: 'BANNED',
canEditName: true,
}
})
.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
};
return MailerService.sendSimple(options);
}
}
}, {
new: true,
});
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
};
await MailerService.sendSimple(options);
}
}
return user;
}
/**
+12 -15
View File
@@ -3,14 +3,16 @@ const expect = require('chai').expect;
const User = require('../../../models/user');
const Context = require('../../../graph/context');
const errors = require('../../../errors');
const SettingsService = require('../../../services/settings');
describe('graph.Context', () => {
beforeEach(() => SettingsService.init());
describe('#constructor: with a user', () => {
let c;
beforeEach(() => {
c = new Context({user: new User({id: '1'})});
c = new Context({user: new User({id: '1', roles: ['ADMIN']})});
});
it('creates a context with a user', (done) => {
@@ -21,15 +23,10 @@ describe('graph.Context', () => {
});
it('does have access to mutators', () => {
return c.mutators.Action.create({
item_id: '1',
item_type: 'COMMENTS',
action_type: 'LIKE'
})
.then((action) => {
expect(action).to.have.property('item_id', '1');
expect(action).to.have.property('item_type', 'COMMENTS');
expect(action).to.have.property('action_type', 'LIKE');
return c.mutators.Tag.add({
item_type: 'USERS',
id: '1',
name: 'Tag',
});
});
});
@@ -48,13 +45,13 @@ describe('graph.Context', () => {
});
it('does not have access to mutators', () => {
return c.mutators.Action.create({
item_id: '1',
return c.mutators.Tag.add({
item_type: 'COMMENTS',
action_type: 'LIKE'
id: '1',
name: 'Tag',
})
.then((action) => {
expect(action).to.be.null;
.then(() => {
throw new Error('should not reach this point');
})
.catch((err) => {
expect(err).to.be.equal(errors.ErrNotAuthorized);