+
+ { isFetching && }
+ {
+ hasResults
+ ? commenters.map((commenter, index) => {
+ if (commenter.status === 'PENDING' && commenter.actions.length > 0) {
+ return ;
+ }
+ return null;
+ })
+ : {lang.t('community.no-flagged-accounts')}
+ }
+
+
+ );
+};
+
+export default FlaggedAccounts;
diff --git a/client/coral-admin/src/containers/Community/Community.js b/client/coral-admin/src/containers/Community/People.js
similarity index 94%
rename from client/coral-admin/src/containers/Community/Community.js
rename to client/coral-admin/src/containers/Community/People.js
index 08a962c34..b5701b5a4 100644
--- a/client/coral-admin/src/containers/Community/Community.js
+++ b/client/coral-admin/src/containers/Community/People.js
@@ -29,7 +29,7 @@ const tableHeaders = [
}
];
-const Community = ({isFetching, commenters, ...props}) => {
+const People = ({isFetching, commenters, ...props}) => {
const hasResults = !isFetching && !!commenters.length;
return (
@@ -58,7 +58,7 @@ const Community = ({isFetching, commenters, ...props}) => {
hasResults
?
:
{lang.t('community.no-results')}
@@ -73,4 +73,4 @@ const Community = ({isFetching, commenters, ...props}) => {
);
};
-export default Community;
+export default People;
diff --git a/client/coral-admin/src/containers/Community/Table.js b/client/coral-admin/src/containers/Community/Table.js
index 480ce72bf..aacc9c82f 100644
--- a/client/coral-admin/src/containers/Community/Table.js
+++ b/client/coral-admin/src/containers/Community/Table.js
@@ -77,4 +77,4 @@ class Table extends Component {
}
}
-export default connect(state => ({commenters: state.community.get('commenters')}))(Table);
+export default connect(state => ({commenters: state.community.get('accounts')}))(Table);
diff --git a/client/coral-admin/src/containers/Community/components/ActionButton.js b/client/coral-admin/src/containers/Community/components/ActionButton.js
new file mode 100644
index 000000000..6352480fd
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/components/ActionButton.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import styles from '../Community.css';
+import BanUserButton from '../../../components/BanUserButton';
+import {Button} from 'coral-ui';
+import {menuActionsMap} from '../../../containers/ModerationQueue/helpers/moderationQueueActionsMap';
+
+const ActionButton = ({type = '', user, ...props}) => {
+ if (type === 'BAN') {
+ return
props.showBanUserDialog(user)} />;
+ }
+
+ return (
+ {
+ type === 'APPROVE' ? props.approveUser({userId: user.id}) : props.showSuspendUserDialog({user: user});
+ }}
+ >{menuActionsMap[type].text}
+ );
+};
+
+export default ActionButton;
diff --git a/client/coral-admin/src/containers/Community/components/BanUserDialog.css b/client/coral-admin/src/containers/Community/components/BanUserDialog.css
new file mode 100644
index 000000000..a46b9da32
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/components/BanUserDialog.css
@@ -0,0 +1,164 @@
+.dialog {
+ border: none;
+ box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
+ width: 500px;
+ top: 50%;
+ transform: translateY(-50%);
+ height: 184px;
+ padding: 20px;
+
+ h2 {
+ color: black;
+ font-size: 1.76em;
+ font-weight: 500;
+ margin: 0;
+ }
+
+ h3 {
+ color: black;
+ font-size: 1.4em;
+ font-weight: 500;
+ margin: 0;
+ }
+}
+
+.textField {
+ margin-top: 15px;
+}
+
+.textField label {
+ font-size: 1.08em;
+ font-weight: bold;
+ margin-bottom: 5px;
+}
+
+.textField input {
+ width: 100%;
+ display: block;
+ border: none;
+ outline: none;
+ border: 1px solid rgba(0,0,0,.12);
+ padding: 10px 6px;
+ box-sizing: border-box;
+ border-radius: 2px;
+ margin: 5px auto;
+}
+
+.footer {
+ margin: 20px auto 10px;
+ text-align: center;
+}
+
+.footer span {
+ display: block;
+ margin-bottom: 5px;
+}
+
+.footer a {
+ color: #2c69b6;
+ cursor: pointer;
+ margin: 0 5px;
+}
+
+.socialConnections {
+ margin-bottom: 20px;
+}
+
+.signInButton {
+ margin-top: 10px;
+}
+
+.close {
+ font-size: 20px;
+ line-height: 14px;
+ top: 10px;
+ right: 10px;
+ position: absolute;
+ display: block;
+ font-weight: bold;
+ color: #363636;
+ cursor: pointer;
+}
+
+.close:hover {
+ color: #6b6b6b;
+}
+
+input.error{
+ border: solid 2px #f44336;
+}
+
+.errorMsg, .hint {
+ color: grey;
+ font-weight: 600;
+ padding: 3px 0 16px;
+}
+
+.alert {
+ padding: 10px;
+ margin-bottom: 20px;
+ border-radius: 2px;
+}
+
+.alert--success {
+ border: solid 1px #1ec00e;
+ background: #cbf1b8;
+ color: #006900;
+}
+
+.alert--error {
+ background: #FFEBEE;
+ color: #B71C1C;
+}
+
+.userBox a {
+ color: #2c69b6;
+ cursor: pointer;
+ margin: 0px;
+}
+
+.attention {
+ display: inline-block;
+ width: 15px;
+ height: 15px;
+ background: #B71C1C;
+ color: #FFEBEE;
+ font-weight: bolder;
+ padding: 4px;
+ vertical-align: middle;
+ border-radius: 20px;
+ box-sizing: border-box;
+ font-size: 9px;
+ line-height: 7px;
+ text-align: center;
+ margin-right: 5px;
+}
+
+.action {
+ margin-top: 15px;
+}
+
+.passwordRequestSuccess {
+ border: 1px solid green;
+ background-color: lightgreen;
+ padding: 10px;
+}
+
+.passwordRequestFailure {
+ border: 1px solid orange;
+ background-color: 1px solid coral;
+ padding: 10px;
+}
+
+.cancel {
+ margin-right: 10px;
+ width: 47%;
+}
+
+.ban {
+ width: 47%;
+}
+
+.buttons {
+ margin: 20px 0;
+}
diff --git a/client/coral-admin/src/containers/Community/components/BanUserDialog.js b/client/coral-admin/src/containers/Community/components/BanUserDialog.js
new file mode 100644
index 000000000..affffcd54
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/components/BanUserDialog.js
@@ -0,0 +1,54 @@
+import React, {PropTypes} from 'react';
+import {Dialog} from 'coral-ui';
+import styles from './BanUserDialog.css';
+
+import Button from 'coral-ui/components/Button';
+
+import I18n from 'coral-framework/modules/i18n/i18n';
+import translations from 'coral-admin/src/translations.json';
+
+const lang = new I18n(translations);
+
+const BanUserDialog = ({open, handleClose, handleBanUser, user}) => (
+
+ ×
+
+
+
{lang.t('community.ban_user')}
+
+
+
{lang.t('community.are_you_sure', user.username)}
+ {lang.t('community.note')}
+
+
+
+ {lang.t('community.cancel')}
+
+ {
+ handleBanUser({userId: user.id}).then(() => {
+ handleClose();
+ });
+ }}
+ raised>
+ {lang.t('community.yes_ban_user')}
+
+
+
+
+);
+
+BanUserDialog.propTypes = {
+ handleBanUser: PropTypes.func.isRequired,
+ handleClose: PropTypes.func.isRequired,
+ user: PropTypes.object.isRequired,
+};
+
+export default BanUserDialog;
diff --git a/client/coral-admin/src/containers/Community/components/CommunityMenu.js b/client/coral-admin/src/containers/Community/components/CommunityMenu.js
new file mode 100644
index 000000000..9a51798ec
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/components/CommunityMenu.js
@@ -0,0 +1,31 @@
+import React from 'react';
+
+import styles from './styles.css';
+
+import I18n from 'coral-framework/modules/i18n/i18n';
+import translations from 'coral-admin/src/translations.json';
+
+import {Link} from 'react-router';
+
+const lang = new I18n(translations);
+
+const CommunityMenu = () => {
+ const flaggedPath = '/admin/community/flagged';
+ const peoplePath = '/admin/community/people';
+ return (
+
+
+
+
+ {lang.t('community.flaggedaccounts')}
+
+
+ {lang.t('community.people')}
+
+
+
+
+ );
+};
+
+export default CommunityMenu;
diff --git a/client/coral-admin/src/components/SuspendUserModal.css b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.css
similarity index 100%
rename from client/coral-admin/src/components/SuspendUserModal.css
rename to client/coral-admin/src/containers/Community/components/SuspendUserDialog.css
diff --git a/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js
new file mode 100644
index 000000000..44f38f796
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js
@@ -0,0 +1,115 @@
+import React, {Component, PropTypes} from 'react';
+
+import {Dialog, Button} from 'coral-ui';
+import styles from './SuspendUserDialog.css';
+
+import I18n from 'coral-framework/modules/i18n/i18n';
+import translations from 'coral-admin/src/translations.json';
+
+const lang = new I18n(translations);
+
+const stages = [
+ {
+ title: 'suspenduser.title_0',
+ description: 'suspenduser.description_0',
+ options: {
+ 'j': 'suspenduser.no_cancel',
+ 'k': 'suspenduser.yes_suspend'
+ }
+ },
+ {
+ title: 'suspenduser.title_1',
+ description: 'suspenduser.description_1',
+ options: {
+ 'j': 'bandialog.cancel',
+ 'k': 'suspenduser.send'
+ }
+ }
+];
+
+class SuspendUserDialog extends Component {
+
+ state = {email: '', stage: 0}
+
+ static propTypes = {
+ stage: PropTypes.number,
+ handleClose: PropTypes.func.isRequired,
+ suspendUser: PropTypes.func.isRequired
+ }
+
+ componentDidMount() {
+ this.setState({email: lang.t('suspenduser.email'), about: lang.t('suspenduser.username')});
+ }
+
+ /*
+ * When an admin clicks to suspend a user a dialog is shown, this function
+ * handles the possible actions for that dialog.
+ */
+ onActionClick = (stage, menuOption) => () => {
+ const {suspendUser, user} = this.props;
+ const {stage} = this.state;
+
+ const cancel = this.props.onClose;
+ const next = () => this.setState({stage: stage + 1});
+ const suspend = () => {
+ suspendUser({userId: user.user.id, message: this.state.email})
+ .then(() => {
+ this.props.handleClose();
+ });
+ };
+
+ const suspendModalActions = [
+ [ cancel, next ],
+ [ cancel, suspend ]
+ ];
+ return suspendModalActions[stage][menuOption]();
+ }
+
+ onEmailChange = (e) => {
+ this.setState({email: e.target.value});
+ }
+
+ render () {
+ const {open, handleClose} = this.props;
+ const {stage} = this.state;
+
+ return
+
+ {lang.t(stages[stage].title, lang.t('suspenduser.username'))}
+
+
+
+ {lang.t(stages[stage].description, lang.t('suspenduser.username'))}
+
+ {
+ stage === 1 &&
+
+
{lang.t('suspenduser.write_message')}
+
+
+
+ }
+
+ {Object.keys(stages[stage].options).map((key, i) => (
+
+ {lang.t(stages[stage].options[key], lang.t('suspenduser.username'))}
+
+ ))}
+
+
+ ;
+ }
+}
+
+export default SuspendUserDialog;
diff --git a/client/coral-admin/src/containers/Community/components/User.js b/client/coral-admin/src/containers/Community/components/User.js
new file mode 100644
index 000000000..71598e352
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/components/User.js
@@ -0,0 +1,90 @@
+import React from 'react';
+import styles from '../Community.css';
+
+import ActionButton from './ActionButton';
+
+import I18n from 'coral-framework/modules/i18n/i18n';
+import translations from '../../../translations.json';
+
+const lang = new I18n(translations);
+
+// Render a single user for the list
+const User = props => {
+ const {user, modActionButtons} = props;
+ let userStatus = user.status;
+
+ // Do not display unless the user status is 'pending' or 'banned'.
+ // This means that they have already been reviewed and approved.
+ return (userStatus === 'PENDING' || userStatus === 'BANNED') &&
+
+
+
+
+
+ {user.username}
+
+
+
+
+
+
+
+
+ flag Flags({ user.actions.length }) :
+ { user.action_summaries.map(
+ (action, i ) => {
+ return
+ {lang.t(`community.${action.reason}`)} ({action.count})
+ ;
+ }
+ )}
+
+
+ { user.action_summaries.map(
+ (action_sum, i ) => {
+ return
+
+ {lang.t(`community.${action_sum.reason}`)} ({action_sum.count})
+
+ {user.actions.map(
+
+ // find the action by action_sum.reason
+ (action, j) => {
+ if (action.reason === action_sum.reason) {
+ return
+ {action.user && action.user.username}: {action.message ? action.message : 'n/a'}
+
;
+ }
+ return null;
+ }
+ )}
+
;
+ }
+ )}
+
+
+
+
+
+ {modActionButtons.map((action, i) =>
+
+ )}
+
+
+
+
+ ;
+};
+
+export default User;
diff --git a/client/coral-admin/src/containers/Community/components/styles.css b/client/coral-admin/src/containers/Community/components/styles.css
new file mode 100644
index 000000000..2cd3f7b50
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/components/styles.css
@@ -0,0 +1,339 @@
+@custom-media --big-viewport (min-width: 780px);
+
+.listContainer {
+ max-width: 860px;
+ margin: 0 auto;
+}
+
+.tabBar {
+ background-color: rgba(44, 44, 44, 0.89);
+ z-index: 5;
+}
+
+.tab {
+ flex: 1;
+ color: white;
+ text-transform: capitalize;
+ font-weight: 500;
+ font-size: 15px;
+ letter-spacing: 1px;
+ transition: border-bottom 200ms;
+}
+
+.active {
+ color: white;
+ box-sizing: border-box;
+ border-bottom: solid 5px #F36451;
+}
+
+.active > span {
+ color: white;
+}
+
+.active:after {
+ background: transparent !important;
+}
+
+.showShortcuts {
+ position: absolute;
+ right: 130px;
+ display: flex;
+ align-items: center;
+ font-size: 13px;
+
+span {
+ margin-left: 7px;
+}
+}
+
+@media (--big-viewport) {
+ .tab {
+ flex: none;
+ }
+}
+
+.notFound {
+ position: relative;
+ margin: 20px auto;
+ text-align: center;
+ padding: 68px 45px;
+ vertical-align: middle;
+ min-width: 500px;
+
+ a {
+ color: rgb(244, 126, 107);
+ font-weight: 500;
+
+ &.goToStreams {
+ position: absolute;
+ right: 10px;
+ bottom: 10px;
+ }
+ }
+}
+
+.header {
+ background-color: #2c2c2c;
+ color: white;
+ margin-bottom: -1px;
+
+ .settingsButton {
+ i {
+ vertical-align: middle;
+ margin-left: 10px;
+ margin-top: -4px;
+ }
+ }
+
+ .moderateAsset {
+ a {
+ -webkit-box-flex: 1;
+ -ms-flex: 1;
+ flex: 1;
+ color: white;
+ text-transform: capitalize;
+ font-weight: 500;
+ font-size: 15px;
+ letter-spacing: 1px;
+ transition: opacity 200ms;
+ opacity: 1;
+
+ &:hover {
+ opacity: .8;
+ cursor: pointer;
+ }
+
+ &:first-child {
+ text-align: left;
+ }
+
+ &:nth-child(2) {
+ text-align: center;
+ }
+
+ &:last-child {
+ text-align: right;
+ }
+ }
+ }
+}
+
+
+@custom-media --big-viewport (min-width: 780px);
+
+.list {
+ padding: 8px 0;
+ list-style: none;
+ display: block;
+
+ &.singleView .listItem {
+ display: none;
+ }
+
+ &.singleView .listItem.activeItem {
+ display: block;
+ height: 100%;
+ font-size: 1.5em;
+ line-height: 1.5em;
+ border: none;
+
+ .actions {
+ position: fixed;
+ bottom: 60px;
+ left: 25%;
+ display: flex;
+ justify-content: space-around;
+ width: 50%;
+ margin: 0;
+ }
+
+ .actionButton {
+ transform: scale(1.4);
+ }
+ }
+}
+
+.listItem {
+ border-bottom: 1px solid #e0e0e0;
+ font-size: 18px;
+ width: 100%;
+ max-width: 660px;
+ min-width: 400px;
+ margin: 0 auto;
+ position: relative;
+ transition: all 200ms;
+ padding: 10px 0 0;
+ min-height: 220px;
+
+ .container {
+ padding: 0 14px;
+ min-height: 180px;
+ }
+
+ &:hover {
+ box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
+ }
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .context {
+ a {
+ color: #f36451;
+ text-decoration: underline;
+ float: right;
+ }
+ }
+
+ .sideActions {
+ position: absolute;
+ right: 0;
+ height: 100%;
+ top: 0;
+ padding: 40px 18px;
+ box-sizing: border-box;
+ }
+
+ .itemHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .author {
+ min-width: 230px;
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ .itemBody {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .avatar {
+ margin-right: 16px;
+ height: 40px;
+ width: 40px;
+ border-radius: 50%;
+ background-color: #757575;
+ font-size: 40px;
+ color: #fff;
+ }
+
+ .created {
+ color: #666;
+ font-size: 13px;
+ margin-left: 40px;
+ }
+
+ .actionButton {
+ transform: scale(.8);
+ margin: 0;
+ }
+
+ .body {
+ margin-top: 0px;
+ flex: 1;
+ color: black;
+ max-width: 500px;
+ word-wrap: break-word;
+ font-weight: 300;
+ }
+
+ .flagged {
+ color: rgba(255, 0, 0, .5);
+ padding-top: 15px;
+ padding-left: 10px;
+ }
+
+ .flagCount{
+ font-size: 12px;
+ color: #d32f2f;
+ }
+
+}
+
+.empty {
+ color: #444;
+ margin-top: 50px;
+ text-align: center;
+}
+
+
+@media (--big-viewport) {
+ .listItem {
+ border: 1px solid #e0e0e0;
+ margin-bottom: 30px;
+
+ &:last-child {
+ border-bottom: 1px solid #e0e0e0;
+ }
+
+ &.activeItem {
+ border: 2px solid #333;
+ }
+ }
+
+}
+
+.hasLinks {
+ color: #f00;
+ text-align: right;
+ display: flex;
+ align-items: center;
+
+ i {
+ margin-right: 5px;
+ }
+}
+
+.banned {
+ color: #f00;
+ text-align: left;
+ display: flex;
+ align-items: center;
+
+ i {
+ margin-right: 5px;
+ }
+}
+
+.ban {
+ display: block;
+ text-align: center;
+ margin-top: 5px;
+}
+
+.Comment {
+ .moderateArticle {
+ font-size: 12px;
+ a {
+ display: inline-block;
+ color: #679af3;
+ text-decoration: none;
+ font-size: 1em;
+ font-weight: 400;
+ letter-spacing: .5px;
+ font-size: 12px;
+ margin-left: 10px;
+
+ &:hover {
+ text-decoration: underline;
+ opacity: .9;
+ cursor: pointer;
+ }
+ }
+ }
+}
+
+.flagBox {
+ max-width: 480px;
+ border-top: 1px solid rgba(66, 66, 66, 0.12);
+ h3 {
+ font-size: 14px;
+ margin: 0;
+ font-weight: 500;
+ }
+}
diff --git a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js
index 36ff7763a..f68bab2b7 100644
--- a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js
+++ b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js
@@ -21,19 +21,19 @@ const Comment = ({actions = [], ...props}) => {
const links = linkify.getMatches(props.comment.body);
const actionSummaries = props.comment.action_summaries;
return (
-
+
-
- {props.comment.user.name}
-
-
- {timeago().format(props.comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
-
- props.showBanUserDialog(props.comment.user, props.comment.id, props.comment.status !== 'REJECTED')} />
-
-
+
+ {props.comment.user.name}
+
+
+ {timeago().format(props.comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
+
+
props.showBanUserDialog(props.comment.user, props.comment.id, props.comment.status !== 'REJECTED')} />
+
+
{props.comment.user.status === 'banned' ?
diff --git a/client/coral-admin/src/graphql/mutations/index.js b/client/coral-admin/src/graphql/mutations/index.js
index 884e30e0c..7ca1b078f 100644
--- a/client/coral-admin/src/graphql/mutations/index.js
+++ b/client/coral-admin/src/graphql/mutations/index.js
@@ -1,6 +1,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';
export const banUser = graphql(SET_USER_STATUS, {
props: ({mutate}) => ({
@@ -9,11 +10,40 @@ export const banUser = graphql(SET_USER_STATUS, {
variables: {
userId,
status: 'BANNED'
- }
+ },
+ refetchQueries: ['Users']
});
}}),
});
+export const setUserStatus = graphql(SET_USER_STATUS, {
+ props: ({mutate}) => ({
+ approveUser: ({userId}) => {
+ return mutate({
+ variables: {
+ userId,
+ status: 'APPROVED'
+ },
+ refetchQueries: ['Users']
+ });
+ }
+ })
+});
+
+export const suspendUser = graphql(SUSPEND_USER, {
+ props: ({mutate}) => ({
+ suspendUser: ({userId, message}) => {
+ return mutate({
+ variables: {
+ userId,
+ message
+ },
+ refetchQueries: ['Users']
+ });
+ }
+ })
+});
+
export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
props: ({mutate}) => ({
acceptComment: ({commentId}) => {
diff --git a/client/coral-admin/src/graphql/mutations/suspendUser.graphql b/client/coral-admin/src/graphql/mutations/suspendUser.graphql
new file mode 100644
index 000000000..0d56a3cc0
--- /dev/null
+++ b/client/coral-admin/src/graphql/mutations/suspendUser.graphql
@@ -0,0 +1,7 @@
+mutation suspendUser($userId: ID!, $message: String) {
+ suspendUser(id: $userId, message: $message) {
+ errors {
+ translation_key
+ }
+ }
+}
diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js
index 13164bca2..291ea6bc5 100644
--- a/client/coral-admin/src/graphql/queries/index.js
+++ b/client/coral-admin/src/graphql/queries/index.js
@@ -2,6 +2,7 @@ import {graphql} from 'react-apollo';
import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
import MOD_QUEUE_LOAD_MORE from './loadMore.graphql';
+import MOD_USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql';
import METRICS from './metricsQuery.graphql';
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
@@ -66,6 +67,8 @@ export const loadMore = (fetchMore) => ({limit, cursor, sort, tab, asset_id}) =>
});
};
+export const modUserFlaggedQuery = graphql(MOD_USER_FLAGGED_QUERY);
+
export const modQueueResort = (id, fetchMore) => (sort) => {
return fetchMore({
query: MOD_QUEUE_QUERY,
diff --git a/client/coral-admin/src/graphql/queries/modUserFlaggedQuery.graphql b/client/coral-admin/src/graphql/queries/modUserFlaggedQuery.graphql
new file mode 100644
index 000000000..98f2191eb
--- /dev/null
+++ b/client/coral-admin/src/graphql/queries/modUserFlaggedQuery.graphql
@@ -0,0 +1,26 @@
+query Users ($n: ACTION_TYPE) {
+ users (query:{action_type: $n}){
+ id
+ username
+ status
+ roles
+ actions{
+ id
+ created_at
+ ... on FlagAction {
+ reason
+ message
+ user {
+ id
+ username
+ }
+ }
+ }
+ action_summaries {
+ count
+ ... on FlagActionSummary {
+ reason
+ }
+ }
+ }
+}
diff --git a/client/coral-admin/src/reducers/community.js b/client/coral-admin/src/reducers/community.js
index 367e67b2a..d051ae0ff 100644
--- a/client/coral-admin/src/reducers/community.js
+++ b/client/coral-admin/src/reducers/community.js
@@ -6,58 +6,88 @@ import {
FETCH_COMMENTERS_SUCCESS,
SORT_UPDATE,
SET_ROLE,
- SET_COMMENTER_STATUS
+ SET_COMMENTER_STATUS,
+ SHOW_BANUSER_DIALOG,
+ HIDE_BANUSER_DIALOG,
+ SHOW_SUSPENDUSER_DIALOG,
+ HIDE_SUSPENDUSER_DIALOG
} from '../constants/community';
const initialState = Map({
community: Map(),
- isFetching: false,
- error: '',
- commenters: [],
- field: 'created_at',
- asc: false,
- totalPages: 0,
- page: 0
+ isFetchingPeople: false,
+ errorPeople: '',
+ accounts: [],
+ fieldPeople: 'created_at',
+ ascPeople: false,
+ totalPagesPeople: 0,
+ pagePeople: 0,
+ user: Map({}),
+ banDialog: false,
+ suspendDialog: false
});
export default function community (state = initialState, action) {
switch (action.type) {
case FETCH_COMMENTERS_REQUEST :
return state
- .set('isFetching', true);
+ .set('isFetchingPeople', true);
case FETCH_COMMENTERS_FAILURE :
return state
- .set('isFetching', false)
- .set('error', action.error);
+ .set('isFetchingPeople', false)
+ .set('errorPeople', action.error);
+
case FETCH_COMMENTERS_SUCCESS : {
- const {commenters, type, ...rest} = action; // eslint-disable-line
+ const {accounts, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line
return state
.merge({
- isFetching: false,
- error: '',
+ isFetchingPeople: false,
+ errorPeople: '',
+ pagePeople: page,
+ countPeople: count,
+ limitPeople: limit,
+ totalPagesPeople: totalPages,
...rest
})
- .set('commenters', commenters); // Sets to normal array
+ .set('accounts', accounts); // Sets to normal array
}
case SET_ROLE : {
- const commenters = state.get('commenters');
+ const commenters = state.get('accounts');
const idx = commenters.findIndex(el => el.id === action.id);
commenters[idx].roles[0] = action.role;
- return state.set('commenters', commenters.map(id => id));
+ return state.set('accounts', commenters.map(id => id));
}
case SET_COMMENTER_STATUS: {
- const commenters = state.get('commenters');
+ const commenters = state.get('accounts');
const idx = commenters.findIndex(el => el.id === action.id);
commenters[idx].status = action.status;
- return state.set('commenters', commenters.map(id => id));
+ return state.set('accounts', commenters.map(id => id));
}
case SORT_UPDATE :
return state
- .set('field', action.sort.field)
- .set('asc', !state.get('asc'));
+ .set('fieldPeople', action.sort.field)
+ .set('ascPeople', !state.get('ascPeople'));
+ case HIDE_BANUSER_DIALOG:
+ return state
+ .set('banDialog', false);
+ case SHOW_BANUSER_DIALOG:
+ return state
+ .merge({
+ user: Map(action.user),
+ banDialog: true
+ });
+ case HIDE_SUSPENDUSER_DIALOG:
+ return state
+ .set('suspendDialog', false);
+ case SHOW_SUSPENDUSER_DIALOG:
+ return state
+ .merge({
+ user: Map(action.user),
+ suspendDialog: true
+ });
default :
return state;
}
diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json
index 13538e224..f59499e50 100644
--- a/client/coral-admin/src/translations.json
+++ b/client/coral-admin/src/translations.json
@@ -17,7 +17,20 @@
"active": "Active",
"banned": "Banned",
"banned-user": "Banned User",
- "loading": "Loading results"
+ "loading": "Loading results",
+ "flaggedaccounts": "Flagged Usernames",
+ "people": "People",
+ "no-flagged-accounts": "The Account Flags queue is currently empty.",
+ "I don't like this username": "I don't like this username",
+ "This user is impersonating": "Impersonation",
+ "This looks like an ad/marketing": "Spam/Ads",
+ "This username is offensive": "Offensive",
+ "Other": "Other",
+ "ban_user": "Ban User?",
+ "are_you_sure": "Are you sure you would like to ban {0}?",
+ "note": "Note: Banning this user will not let them edit, comment or remove anything.",
+ "cancel": "Cancel",
+ "yes_ban_user": "Yes, Ban User"
},
"modqueue": {
"likes": "likes",
@@ -104,8 +117,9 @@
"yes_ban_user": "Yes, Ban User"
},
"suspenduser": {
- "title_0": "We noticed you rejected a {0}",
- "description_0": "Would you like to temporarily ban this user becuase of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}.",
+ "title": "Suspend a user",
+ "title_0": "We noticed you rejected a username",
+ "description_0": "Would you like to temporarily ban this user because of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}.",
"title_1": "Notify the user of their temporary suspension",
"description_1": "Suspending this user will temporarily disable their account and hide all of their comments on the site.",
"no_cancel": "No, cancel",
@@ -114,7 +128,7 @@
"bio": "bio",
"username": "username",
"email_subject": "Your account has been suspended",
- "email": "Another member of the community recently flagged your {0} for review. Because of its content your {0} was rejected. This means you can no longer comment, like, or flag content until you rewrite your {0}. Please e-mail moderator@newsorg.com if you have any questions or concerns.",
+ "email": "Another member of the community recently flagged your username for review. Because of its content your user was rejected. This means you can no longer comment, like, or flag content until you rewrite your username. Please e-mail us if you have any questions or concerns.",
"write_message": "Write a message"
},
"dashboard": {
@@ -162,7 +176,34 @@
"active": "Activa",
"banned": "Suspendido",
"banned-user": "Usuario Suspendido",
- "loading": "Cargando resultados"
+ "loading": "Cargando resultados",
+ "flaggedaccounts": "Nombres de Usuario Reportados",
+ "people": "Gente",
+ "no-flagged-accounts": "No hay ninguna cuenta reportada.",
+ "I don't like this username": "No me gusta ese nombre de usuario",
+ "This user is impersonating": "Suplantación",
+ "This looks like an ad/marketing": "Spam/Propaganda",
+ "This username is offensive": "Ofensivo",
+ "Other": "Otros",
+ "ban_user": "Quieres suspender el Usuario?",
+ "are_you_sure": "Estas segura que quieres suspender a {0}?",
+ "note": "Nota: Suspender a este usuario no le va a permitir borrar ni editar ni comentar.",
+ "cancel": "Cancelar",
+ "yes_ban_user": "Si, Suspendan el usuario"
+ },
+ "suspenduser": {
+ "title": "Suspendiendo un usuario",
+ "title_0": "Esta queriendo suspender un usuario?",
+ "description_0": "Le gustaria suspender a esta usuaria temporarianmente por su nombre de usuario? Si lo hace sus comentarios serán escondidos temporariamente hasta que puedan reescribir su nombre de usuario.",
+ "title_1": "Enviarle una nota al usuario sobre su cuenta suspendida",
+ "description_1": "Si suspende a este usuario, su cuenta va a ser deshabilitada y todos sus comentarios escondidos del sitio.",
+ "no_cancel": "No, cancelar",
+ "yes_suspend": "Si, suspender",
+ "send": "Enviar",
+ "username": "nombre de usuario",
+ "email_subject": "Su cuenta ha sido suspendida temporariamente",
+ "email": "Otra persona de la comunidad recientemente marcó su nombre de usuario para ser revisado. Por su contenido, el nombre de usuario ha sido rechazado. Esto quiere decir que no puede comentar, gustar o marcar contenido hasta que modifique su nombre de usuario. Por favor, envienos un correo a moderator@newsorg.com si tiene alguna pregunta o preocupación",
+ "write_message": "Escribir un mensaje"
},
"modqueue": {
"likes": "gustos",
diff --git a/client/coral-plugin-flags/FlagComment.js b/client/coral-plugin-flags/FlagComment.js
index 24fde6b0f..3f3051f28 100644
--- a/client/coral-plugin-flags/FlagComment.js
+++ b/client/coral-plugin-flags/FlagComment.js
@@ -23,14 +23,14 @@ const getPopupMenu = [
{val: 'This comment is offensive', text: lang.t('comment-offensive')},
{val: 'This looks like an ad/marketing', text: lang.t('marketing')},
{val: 'I don\'t agree with this comment', text: lang.t('no-agree-comment')},
- {val: 'other', text: lang.t('other')}
+ {val: 'Other', text: lang.t('other')}
]
: [
{val: 'This username is offensive', text: lang.t('username-offensive')},
{val: 'I don\'t like this username', text: lang.t('no-like-username')},
{val: 'This user is impersonating', text: lang.t('user-impersonating')},
{val: 'This looks like an ad/marketing', text: lang.t('marketing')},
- {val: 'other', text: lang.t('other')}
+ {val: 'Other', text: lang.t('other')}
];
return {
header: lang.t('step-2-header'),
diff --git a/graph/loaders/users.js b/graph/loaders/users.js
index 90c661f71..145b0dfdb 100644
--- a/graph/loaders/users.js
+++ b/graph/loaders/users.js
@@ -3,11 +3,51 @@ const DataLoader = require('dataloader');
const util = require('./util');
const UsersService = require('../../services/users');
+const UserModel = require('../../models/user');
const genUserByIDs = (context, ids) => UsersService
.findByIdArray(ids)
.then(util.singleJoinBy(ids, 'id'));
+/**
+ * Retrieves users based on the passed in query that is filtered by the
+ * current used passed in via the context.
+ * @param {Object} context graph context
+ * @param {Object} query query terms to apply to the users query
+ */
+const getUsersByQuery = ({user}, {ids, limit, cursor, sort}) => {
+
+ let users = UserModel.find();
+
+ if (ids) {
+ users = users.find({
+ id: {
+ $in: ids
+ }
+ });
+ }
+
+ if (cursor) {
+ if (sort === 'REVERSE_CHRONOLOGICAL') {
+ users = users.where({
+ created_at: {
+ $lt: cursor
+ }
+ });
+ } else {
+ users = users.where({
+ created_at: {
+ $gt: cursor
+ }
+ });
+ }
+ }
+
+ return users
+ .sort({created_at: sort === 'REVERSE_CHRONOLOGICAL' ? -1 : 1})
+ .limit(limit);
+};
+
/**
* Creates a set of loaders based on a GraphQL context.
* @param {Object} context the context of the GraphQL request
@@ -15,6 +55,7 @@ const genUserByIDs = (context, ids) => UsersService
*/
module.exports = (context) => ({
Users: {
+ getByQuery: (query) => getUsersByQuery(context, query),
getByID: new DataLoader((ids) => genUserByIDs(context, ids))
}
});
diff --git a/graph/mutators/user.js b/graph/mutators/user.js
index 3f87c1fb5..48966c5e8 100644
--- a/graph/mutators/user.js
+++ b/graph/mutators/user.js
@@ -6,18 +6,28 @@ const setUserStatus = ({user}, {id, status}) => {
.then(res => res);
};
-module.exports = (context) => {
- if (context.user && context.user.can('mutation:setUserStatus')) {
- return {
- User: {
- setUserStatus: (action) => setUserStatus(context, action)
- }
- };
- }
+const suspendUser = ({user}, {id, message}) => {
+ return UsersService.suspendUser(id, message)
+ .then(res => {
+ return res;
+ });
+};
- return {
+module.exports = (context) => {
+ let mutators = {
User: {
- setUserStatus: () => Promise.reject(errors.ErrNotAuthorized)
+ setUserStatus: () => Promise.reject(errors.ErrNotAuthorized),
+ suspendUser: () => Promise.reject(errors.ErrNotAuthorized)
}
};
+
+ if (context.user && context.user.can('mutation:setUserStatus')) {
+ mutators.User.setUserStatus = (action) => setUserStatus(context, action);
+ }
+
+ if (context.user && context.user.can('mutation:suspendUser')) {
+ mutators.User.suspendUser = (action) => suspendUser(context, action);
+ }
+
+ return mutators;
};
diff --git a/graph/resolvers/flag_action.js b/graph/resolvers/flag_action.js
index 44cf7a410..80d1583ad 100644
--- a/graph/resolvers/flag_action.js
+++ b/graph/resolvers/flag_action.js
@@ -1,9 +1,15 @@
const FlagAction = {
// Stored in the metadata, extract and return.
- reason({metadata: {reason}}) {
- return reason;
- }
+ message({metadata: {message}}) {
+ return message;
+ },
+ reason({group_id}) {
+ return group_id;
+ },
+ user({user_id}, _, {loaders: {Users}}) {
+ return Users.getByID.load(user_id);
+ },
};
module.exports = FlagAction;
diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js
index dc540b202..a474407d2 100644
--- a/graph/resolvers/root_mutation.js
+++ b/graph/resolvers/root_mutation.js
@@ -47,6 +47,9 @@ 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}));
+ },
setCommentStatus(_, {id, status}, {mutators: {Comment}}) {
return wrapResponse(null)(Comment.setCommentStatus({id, status}));
},
diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js
index e2836c51d..ce11fcae9 100644
--- a/graph/resolvers/root_query.js
+++ b/graph/resolvers/root_query.js
@@ -86,6 +86,28 @@ const RootQuery = {
}
return user;
+ },
+
+ // This endpoint is used for loading the user moderation queues (users whose username has been flagged),
+ // so hide it in the event that we aren't an admin.
+ users(_, {query: {action_type, limit, cursor, sort}}, {user, loaders: {Users, Actions}}) {
+
+ if (user == null || !user.hasRoles('ADMIN')) {
+ return null;
+ }
+
+ const query = {limit, cursor, sort};
+
+ if (action_type) {
+ return Actions.getByTypes({action_type, item_type: 'USERS'})
+ .then((ids) => {
+
+ // Perform the query using the available resolver.
+ return Users.getByQuery({ids, limit, cursor, sort});
+ });
+ }
+
+ return Users.getByQuery(query);
}
};
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index e656d0183..8b07c2ab2 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -45,6 +45,9 @@ type User {
# returns all comments based on a query.
comments(query: CommentsQuery): [Comment]
+ # returns all users based on a query.
+ users(query: UsersQuery): [User]
+
# returns user status
status: USER_STATUS
}
@@ -60,6 +63,20 @@ type Tag {
created_at: Date!
}
+# UsersQuery allows the ability to query users by a specific fields.
+input UsersQuery {
+ action_type: ACTION_TYPE
+
+ # Limit the number of results to be returned.
+ limit: Int = 10
+
+ # Skip results from the last created_at timestamp.
+ cursor: Date
+
+ # Sort the results by created_at.
+ sort: SORT_ORDER = REVERSE_CHRONOLOGICAL
+}
+
################################################################################
## Comments
################################################################################
@@ -520,6 +537,9 @@ type RootQuery {
# role.
me: User
+ # Users returned based on a query.
+ users(query: UsersQuery): [User]
+
# Asset metrics related to user actions are saturated into the assets
# returned.
assetMetrics(from: Date!, to: Date!, sort: ASSET_METRICS_SORT!, limit: Int = 10): [Asset!]
@@ -653,6 +673,14 @@ type SetUserStatusResponse implements Response {
errors: [UserError]
}
+# SuspendUserResponse is the response returned with possibly some errors
+# relating to the suspend action attempt.
+type SuspendUserResponse 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 {
@@ -696,6 +724,9 @@ 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
+
# 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 e23acaa56..e1cbb466b 100644
--- a/models/user.js
+++ b/models/user.js
@@ -176,9 +176,10 @@ const USER_GRAPH_OPERATIONS = [
'mutation:deleteAction',
'mutation:editName',
'mutation:setUserStatus',
+ 'mutation:suspendUser',
'mutation:setCommentStatus',
'mutation:addCommentTag',
- 'mutation:removeCommentTag',
+ 'mutation:removeCommentTag'
];
/**
@@ -194,7 +195,7 @@ UserSchema.method('can', function(...actions) {
return false;
}
- if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:setCommentStatus') && !this.hasRoles('ADMIN')) {
+ if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:suspendUser' || action === 'mutation:setCommentStatus') && !this.hasRoles('ADMIN')) {
return false;
}
diff --git a/routes/api/account/index.js b/routes/api/account/index.js
index ee10636bf..44b4b3953 100644
--- a/routes/api/account/index.js
+++ b/routes/api/account/index.js
@@ -59,8 +59,7 @@ router.post('/password/reset', (req, res, next) => {
}
return mailer.sendSimple({
- app: req.app, // needed to render the templates.
- template: 'email/password-reset', // needed to know which template to render!
+ template: 'password-reset', // needed to know which template to render!
locals: { // specifies the template locals.
token,
rootURL: process.env.TALK_ROOT_URL
diff --git a/routes/api/users/index.js b/routes/api/users/index.js
index 2af6bc2ee..a01fed2c9 100644
--- a/routes/api/users/index.js
+++ b/routes/api/users/index.js
@@ -76,8 +76,7 @@ router.post('/:user_id/email', authorization.needed('ADMIN'), (req, res, next) =
if (localProfile) {
const options =
{
- app: req.app, // needed to render the templates.
- template: 'email/notification', // needed to know which template to render!
+ template: 'notification', // needed to know which template to render!
locals: { // specifies the template locals.
body: req.body.body
},
@@ -106,8 +105,7 @@ const SendEmailConfirmation = (app, userID, email, referer) => UsersService
.createEmailConfirmToken(userID, email, referer)
.then((token) => {
return mailer.sendSimple({
- app, // needed to render the templates.
- template: 'email/email-confirm', // needed to know which template to render!
+ template: 'email-confirm', // needed to know which template to render!
locals: { // specifies the template locals.
token,
rootURL: process.env.TALK_ROOT_URL,
diff --git a/views/email/email-confirm.ejs b/services/email/email-confirm.ejs
similarity index 100%
rename from views/email/email-confirm.ejs
rename to services/email/email-confirm.ejs
diff --git a/views/email/email-confirm.txt.ejs b/services/email/email-confirm.txt.ejs
similarity index 100%
rename from views/email/email-confirm.txt.ejs
rename to services/email/email-confirm.txt.ejs
diff --git a/views/email/notification.ejs b/services/email/notification.ejs
similarity index 100%
rename from views/email/notification.ejs
rename to services/email/notification.ejs
diff --git a/views/email/notification.txt.ejs b/services/email/notification.txt.ejs
similarity index 100%
rename from views/email/notification.txt.ejs
rename to services/email/notification.txt.ejs
diff --git a/views/email/password-reset.ejs b/services/email/password-reset.ejs
similarity index 100%
rename from views/email/password-reset.ejs
rename to services/email/password-reset.ejs
diff --git a/views/email/password-reset.txt.ejs b/services/email/password-reset.txt.ejs
similarity index 100%
rename from views/email/password-reset.txt.ejs
rename to services/email/password-reset.txt.ejs
diff --git a/services/email/suspension.ejs b/services/email/suspension.ejs
new file mode 100644
index 000000000..b36560ec5
--- /dev/null
+++ b/services/email/suspension.ejs
@@ -0,0 +1 @@
+<%= body %>
diff --git a/services/email/suspension.txt.ejs b/services/email/suspension.txt.ejs
new file mode 100644
index 000000000..b36560ec5
--- /dev/null
+++ b/services/email/suspension.txt.ejs
@@ -0,0 +1 @@
+<%= body %>
diff --git a/services/mailer.js b/services/mailer.js
index 6615ee9dc..2928570ad 100644
--- a/services/mailer.js
+++ b/services/mailer.js
@@ -1,6 +1,9 @@
const debug = require('debug')('talk:services:mailer');
const nodemailer = require('nodemailer');
const kue = require('./kue');
+const path = require('path');
+const fs = require('fs');
+const _ = require('lodash');
const smtpRequiredProps = [
'TALK_SMTP_FROM_ADDRESS',
@@ -13,6 +16,20 @@ if (smtpRequiredProps.some(prop => !process.env[prop])) {
console.error(`${smtpRequiredProps.join(', ')} should be defined in the environment if you would like to send password reset emails from Talk`);
}
+// load all the templates as strings
+const templateStrings = {};
+fs.readdir(path.join(__dirname, 'email'), (err, files) => {
+ if (err) {
+ throw err;
+ }
+
+ files.forEach(file => {
+ fs.readFile(path.join(__dirname, 'email', file), 'utf8', (err, data) => {
+ templateStrings[file] = _.template(data);
+ });
+ });
+});
+
const options = {
host: process.env.TALK_SMTP_HOST,
auth: {
@@ -38,25 +55,7 @@ const mailer = module.exports = {
name: 'mailer'
}),
- /**
- * Render renders the template with the given locals and returns the rendered
- * html/text.
- */
- render(app, template, locals = {}) {
- return new Promise((resolve, reject) => {
-
- // Render the template with the app.render method.
- app.render(template, locals, (err, rendered) => {
- if (err) {
- return reject(err);
- }
-
- return resolve(rendered);
- });
- });
- },
-
- sendSimple({app, template, locals, to, subject}) {
+ sendSimple({template, locals, to, subject}) {
if (!to) {
return Promise.reject('sendSimple requires a comma-separated list of "to" addresses');
}
@@ -71,10 +70,10 @@ const mailer = module.exports = {
return Promise.all([
// Render the HTML version of the email.
- mailer.render(app, `${template}.ejs`, locals),
+ templateStrings[`${template}.ejs`](locals),
// Render the TEXT version of the email.
- mailer.render(app, `${template}.txt.ejs`, locals)
+ templateStrings[`${template}.txt.ejs`](locals)
])
.then(([html, text]) => {
diff --git a/services/users.js b/services/users.js
index 1810cf2a5..4e1196252 100644
--- a/services/users.js
+++ b/services/users.js
@@ -1,8 +1,11 @@
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const Wordlist = require('./wordlist');
+
const errors = require('../errors');
+
const uuid = require('uuid');
+
const redis = require('./redis');
const redisClient = redis.createClient();
@@ -14,6 +17,7 @@ const RECAPTCHA_WINDOW_SECONDS = 60 * 10; // 10 minutes.
const RECAPTCHA_INCORRECT_TRIGGER = 5; // after 3 incorrect attempts, recaptcha will be required.
const ActionsService = require('./actions');
+const MailerService = require('./mailer');
// In the event that the TALK_SESSION_SECRET is missing but we are testing, then
// set the process.env.TALK_SESSION_SECRET.
@@ -446,6 +450,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
+ */
+ static suspendUser(id, message) {
+ return UserModel.update({
+ id
+ }, {
+ $set: {
+ status: 'BANNED',
+ canEditName: true
+ }
+ })
+ .then(() => {
+ return UsersService.findById(id)
+ .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);
+ } else {
+ return Promise.reject(errors.ErrMissingEmail);
+ }
+ }
+ });
+
+ });
+ }
+
/**
* Finds a user with the id.
* @param {String} id user id (uuid)
diff --git a/test/graph/mutations/addCommentTag.js b/test/graph/mutations/addCommentTag.js
index 14a746705..0835e81bc 100644
--- a/test/graph/mutations/addCommentTag.js
+++ b/test/graph/mutations/addCommentTag.js
@@ -54,7 +54,7 @@ describe('graph.mutations.addCommentTag', () => {
}
expect(response.errors).to.be.empty;
expect(response.data.addCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]);
- expect(response.data.addCommentTag.comment).to.be.null;
+ expect(response.data.addCommentTag.comment).to.be.null;
});
});
});