diff --git a/INSTALL.md b/INSTALL.md index 008c4cba3..33b634de8 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -15,7 +15,7 @@ There are some runtime requirements for running Talk from source: - [Node](https://nodejs.org/) v7 or later -- [MongoDB](https://www.mongodb.com/) v3.2 or later +- [MongoDB](https://www.mongodb.com/) v3.4 or later - [Redis](https://redis.io/) v3.2 or later - [Yarn](https://yarnpkg.com/) v0.19.1 or later diff --git a/bin/cli-setup b/bin/cli-setup index 185b04f01..7985bf220 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -116,11 +116,11 @@ const performSetup = () => { return inquirer.prompt([ { type: 'input', - name: 'displayName', - message: 'Display Name', - filter: (displayName) => { + name: 'username', + message: 'Username', + filter: (username) => { return UsersService - .isValidDisplayName(displayName, false) + .isValidDisplayName(username, false) .catch((err) => { throw err.message; }); @@ -174,7 +174,7 @@ const performSetup = () => { settings: settings.toObject(), user: { email: user.email, - displayName: user.displayName, + username: user.username, password: user.password } }); diff --git a/bin/cli-users b/bin/cli-users index 01f46ad2e..473c07d9c 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -32,7 +32,7 @@ function getUserCreateAnswers(options) { email: options.email, password: options.password, confirmPassword: options.password, - displayName: options.name, + username: options.name, roles: [] }; @@ -75,11 +75,11 @@ function getUserCreateAnswers(options) { } }, { - name: 'displayName', - message: 'Display Name', - filter: (displayName) => { + name: 'username', + message: 'Username', + filter: (username) => { return UsersService - .isValidDisplayName(displayName) + .isValidDisplayName(username) .catch((err) => { throw err.message; }); @@ -108,7 +108,7 @@ function createUser(options) { }) .then((answers) => { return UsersService - .createLocalUser(answers.email.trim(), answers.password.trim(), answers.displayName.trim()) + .createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim()) .then((user) => { console.log(`Created user ${user.id}.`); @@ -209,7 +209,7 @@ function updateUser(userID, options) { 'id': userID }, { $set: { - displayName: options.name + username: options.name } }); @@ -238,7 +238,7 @@ function listUsers() { let table = new Table({ head: [ 'ID', - 'Display Name', + 'Username', 'Profiles', 'Roles', 'Status', @@ -249,7 +249,7 @@ function listUsers() { users.forEach((user) => { table.push([ user.id, - user.displayName, + user.username, user.profiles.map((p) => p.provider).join(', '), user.roles.join(', '), user.status, diff --git a/circle.yml b/circle.yml index bae929df7..77e61f102 100644 --- a/circle.yml +++ b/circle.yml @@ -10,11 +10,21 @@ machine: dependencies: override: + # TODO: use the following to add in support for MongoDB 3.4. + # # Upgrade the database version to 3.4. + # - sudo apt-get purge mongodb-org* + # - sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6 + # - echo "deb [ arch=amd64 ] http://repo.mongodb.org/apt/ubuntu precise/mongodb-org/3.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.4.list + # - sudo apt-get update + # - sudo apt-get install -y mongodb-org + # - sudo service mongod restart + + # Install node dependencies. - yarn cache_directories: - ~/.cache/yarn post: - # Build the static assets + # Build the static assets. - yarn build # Lint the project here, before tests are ran. - yarn lint @@ -30,7 +40,7 @@ test: override: # Run the tests using the junit reporter. - MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test - # Run the e2e test suite + # Run the e2e test suite. - E2E_REPORT_PATH=$CIRCLE_TEST_REPORTS/e2e yarn e2e deployment: diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 9721123fa..a00c17c04 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -18,6 +18,9 @@ const routes = ( + + + ); diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/assets.js index f431f1ad6..a4a3513a8 100644 --- a/client/coral-admin/src/actions/assets.js +++ b/client/coral-admin/src/actions/assets.js @@ -4,8 +4,10 @@ import { FETCH_ASSETS_FAILURE, UPDATE_ASSET_STATE_REQUEST, UPDATE_ASSET_STATE_SUCCESS, - UPDATE_ASSET_STATE_FAILURE + UPDATE_ASSET_STATE_FAILURE, + UPDATE_ASSETS } from '../constants/assets'; + import coralApi from '../../../coral-framework/helpers/response'; /** @@ -34,3 +36,7 @@ export const updateAssetState = (id, closedAt) => (dispatch) => { dispatch({type: UPDATE_ASSET_STATE_SUCCESS})) .catch(error => dispatch({type: UPDATE_ASSET_STATE_FAILURE, error})); }; + +export const updateAssets = assets => dispatch => { + dispatch({type: UPDATE_ASSETS, assets}); +}; diff --git a/client/coral-admin/src/actions/comments.js b/client/coral-admin/src/actions/comments.js index ac7af12fd..14f33bf36 100644 --- a/client/coral-admin/src/actions/comments.js +++ b/client/coral-admin/src/actions/comments.js @@ -101,12 +101,3 @@ export const flagComment = id => (dispatch, getState) => { dispatch({type: commentTypes.COMMENT_FLAG, id}); dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)}); }; - -// Dialog Actions -export const showBanUserDialog = (userId, userName, commentId) => { - return {type: commentTypes.SHOW_BANUSER_DIALOG, userId, userName, commentId}; -}; - -export const hideBanUserDialog = (showDialog) => { - return {type: commentTypes.HIDE_BANUSER_DIALOG, showDialog}; -}; diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js new file mode 100644 index 000000000..b332b9086 --- /dev/null +++ b/client/coral-admin/src/actions/moderation.js @@ -0,0 +1,9 @@ +import * as actions from 'constants/moderation'; + +export const setActiveTab = activeTab => ({type: actions.SET_ACTIVE_TAB, activeTab}); +export const toggleModal = open => ({type: actions.TOGGLE_MODAL, open}); +export const singleView = () => ({type: actions.SINGLE_VIEW}); + +// Ban User Dialog +export const showBanUserDialog = (user, commentId) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId}); +export const hideBanUserDialog = (showDialog) => ({type: actions.HIDE_BANUSER_DIALOG, showDialog}); diff --git a/client/coral-admin/src/components/ActionButton.js b/client/coral-admin/src/components/ActionButton.js index 61de1c669..1d1d4d9d0 100644 --- a/client/coral-admin/src/components/ActionButton.js +++ b/client/coral-admin/src/components/ActionButton.js @@ -1,48 +1,22 @@ import React from 'react'; import styles from './ModerationList.css'; -import I18n from 'coral-framework/modules/i18n/i18n'; -import translations from '../translations.json'; -import {FabButton, Button, Icon} from 'coral-ui'; +import BanUserButton from './BanUserButton'; +import {FabButton} from 'coral-ui'; +import {menuActionsMap} from '../containers/ModerationQueue/helpers/moderationQueueActionsMap'; -const ActionButton = ({option, type, comment = {}, user, menuOptionsMap, onClickAction, onClickShowBanDialog}) => -{ - const banned = user.status === 'BANNED'; +const ActionButton = ({type = '', user, ...props}) => { + if (type === 'BAN') { + return props.showBanUserDialog(props.user, props.id)} />; + } - if (option === 'flag' && (type === 'USERS' || comment.status || comment.flagged === true)) { - return null; - } - if (option === 'ban') { - return ( -
- -
- ); - } - const menuOption = menuOptionsMap[option]; - const action = { - item_type: type, - item_id: type === 'COMMENTS' ? comment.id : user.id - }; return ( onClickAction(menuOption.status, type === 'COMMENTS' ? comment : user, action)} + className={`${type.toLowerCase()} ${styles.actionButton}`} + cStyle={type.toLowerCase()} + icon={menuActionsMap[type].icon} + onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment} /> ); }; export default ActionButton; - -const lang = new I18n(translations); diff --git a/client/coral-admin/src/components/BanUserButton.css b/client/coral-admin/src/components/BanUserButton.css new file mode 100644 index 000000000..79b805c30 --- /dev/null +++ b/client/coral-admin/src/components/BanUserButton.css @@ -0,0 +1,10 @@ +.banButton { + width: 114px; + letter-spacing: 1px; + + i { + vertical-align: middle; + margin-right: 10px; + font-size: 14px; + } +} diff --git a/client/coral-admin/src/components/BanUserButton.js b/client/coral-admin/src/components/BanUserButton.js new file mode 100644 index 000000000..191164ca5 --- /dev/null +++ b/client/coral-admin/src/components/BanUserButton.js @@ -0,0 +1,26 @@ +import React, {PropTypes} from 'react'; +import {Button, Icon} from 'coral-ui'; +import styles from './BanUserButton.css'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from 'coral-admin/src/translations.json'; +const lang = new I18n(translations); + +const BanUserButton = ({user, ...props}) => ( +
+ +
+); + +BanUserButton.propTypes = { + onClick: PropTypes.func.isRequired +}; + +export default BanUserButton; diff --git a/client/coral-admin/src/components/BanUserDialog.js b/client/coral-admin/src/components/BanUserDialog.js index 0ad149fbe..b23d8f9d1 100644 --- a/client/coral-admin/src/components/BanUserDialog.js +++ b/client/coral-admin/src/components/BanUserDialog.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {PropTypes} from 'react'; import {Dialog} from 'coral-ui'; import styles from './BanUserDialog.css'; @@ -8,34 +8,28 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; const lang = new I18n(translations); -const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => ( +const BanUserDialog = ({open, handleClose, handleBanUser, user}) => ( handleClose()} - onCancel={() => handleClose()} + onClose={handleClose} + onCancel={handleClose} title={lang.t('bandialog.ban_user')}> ×
-

- {lang.t('bandialog.ban_user')} -

+

{lang.t('bandialog.ban_user')}

-

- {lang.t('bandialog.are_you_sure', user.userName)} -

- - {lang.t('bandialog.note')} - +

{lang.t('bandialog.are_you_sure', user.name)}

+ {lang.t('bandialog.note')}
- -
@@ -43,4 +37,10 @@ const BanUserDialog = ({open, handleClose, onClickBanUser, 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/components/Comment.js b/client/coral-admin/src/components/Comment.js index 74b0361a2..db5cfe5bf 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -23,7 +23,7 @@ const Comment = props => {
  • - {author.displayName || lang.t('comment.anon')} + {author.username || lang.t('comment.anon')} {timeago().format(comment.createdAt || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))} {comment.flagged ?

    {lang.t('comment.flagged')}

    : null}
    diff --git a/client/coral-admin/src/components/ModerationList.css b/client/coral-admin/src/components/ModerationList.css index fbcbce7d7..140f4bc8f 100644 --- a/client/coral-admin/src/components/ModerationList.css +++ b/client/coral-admin/src/components/ModerationList.css @@ -96,11 +96,6 @@ margin-left: 40px; } - .actionButton { - transform: scale(.8); - margin: 0; - } - .body { margin-top: 20px; flex: 1; @@ -182,3 +177,9 @@ font-size: 14px; } } + + +.actionButton { + transform: scale(.8); + margin: 0; +} diff --git a/client/coral-admin/src/components/User.js b/client/coral-admin/src/components/User.js index 54de0375e..129524993 100644 --- a/client/coral-admin/src/components/User.js +++ b/client/coral-admin/src/components/User.js @@ -18,16 +18,15 @@ const User = props => {
  • - {user.displayName} + {user.username}
    {props.modActions.map( (action, i) => ( !restricted ?
    - {lang.t('configure.moderate')} + + {lang.t('configure.streams')} + {lang.t('configure.community')} @@ -25,10 +29,6 @@ export default ({handleLogout, restricted = false}) => ( activeClassName={styles.active}> {lang.t('configure.configure')} - - {lang.t('configure.streams')} -
      diff --git a/client/coral-admin/src/constants/assets.js b/client/coral-admin/src/constants/assets.js index 0a2ecf73c..20ec0a9c3 100644 --- a/client/coral-admin/src/constants/assets.js +++ b/client/coral-admin/src/constants/assets.js @@ -1,6 +1,9 @@ export const FETCH_ASSETS_REQUEST = 'FETCH_ASSETS_REQUEST'; export const FETCH_ASSETS_SUCCESS = 'FETCH_ASSETS_SUCCESS'; export const FETCH_ASSETS_FAILURE = 'FETCH_ASSETS_FAILURE'; + export const UPDATE_ASSET_STATE_REQUEST = 'UPDATE_ASSET_STATE_REQUEST'; export const UPDATE_ASSET_STATE_SUCCESS = 'UPDATE_ASSET_STATE_SUCCESS'; export const UPDATE_ASSET_STATE_FAILURE = 'UPDATE_ASSET_STATE_FAILURE'; + +export const UPDATE_ASSETS = 'UPDATE_ASSETS'; diff --git a/client/coral-admin/src/constants/moderation.js b/client/coral-admin/src/constants/moderation.js new file mode 100644 index 000000000..10c6a7c4c --- /dev/null +++ b/client/coral-admin/src/constants/moderation.js @@ -0,0 +1,5 @@ +export const SET_ACTIVE_TAB = 'SET_ACTIVE_TAB'; +export const TOGGLE_MODAL = 'TOGGLE_MODAL'; +export const SINGLE_VIEW = 'SINGLE_VIEW'; +export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG'; +export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG'; diff --git a/client/coral-admin/src/containers/Community/Community.js b/client/coral-admin/src/containers/Community/Community.js index d63dd1e96..bad5654cf 100644 --- a/client/coral-admin/src/containers/Community/Community.js +++ b/client/coral-admin/src/containers/Community/Community.js @@ -13,7 +13,7 @@ const lang = new I18n(translations); const tableHeaders = [ { title: lang.t('community.username_and_email'), - field: 'displayName' + field: 'username' }, { title: lang.t('community.account_creation_date'), diff --git a/client/coral-admin/src/containers/Community/Table.js b/client/coral-admin/src/containers/Community/Table.js index 254550e74..480ce72bf 100644 --- a/client/coral-admin/src/containers/Community/Table.js +++ b/client/coral-admin/src/containers/Community/Table.js @@ -44,7 +44,7 @@ class Table extends Component { {commenters.map((row, i)=> ( - {row.displayName} + {row.username} {row.profiles.map(({id}) => id)} diff --git a/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js b/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js index 91c664c37..90deff640 100644 --- a/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js +++ b/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js @@ -21,12 +21,12 @@ const InitialStep = props => { this.setState({singleView: !this.state.singleView})); - key('shift+/', () => this.setState({modalOpen: true})); - key('esc', () => this.setState({modalOpen: false})); + + key('s', () => singleView()); + key('shift+/', () => toggleModal(true)); + key('esc', () => toggleModal(false)); } componentWillUnmount() { @@ -45,90 +36,85 @@ class ModerationContainer extends React.Component { key.unbind('esc'); } - componentDidMount() { - - // Hack for dynamic mdl tabs - if (typeof componentHandler !== 'undefined') { - - // FIXME: fix this hack - componentHandler.upgradeAllRegistered(); // eslint-disable-line no-undef + componentWillReceiveProps(nextProps) { + const {updateAssets} = this.props; + if(!isEqual(nextProps.data.assets, this.props.data.assets)) { + updateAssets(nextProps.data.assets); } } - onTabClick(activeTab) { - this.setState({activeTab}); - - if (activeTab === 'premod') { - this.props.fetchPremodQueue(); - } else if (activeTab === 'rejected') { - this.props.fetchRejectedQueue(); - } else if (activeTab === 'flagged') { - this.props.fetchFlaggedQueue(); - } else { - this.props.fetchModerationQueueComments(); - } - } - - onClose() { - this.setState({modalOpen: false}); - } - render () { - const {comments, actions, settings} = this.props; - const premodIds = comments.ids.filter(id => comments.byId[id].status === 'PREMOD'); - const rejectedIds = comments.ids.filter(id => comments.byId[id].status === 'REJECTED'); - const flaggedIds = comments.ids.filter(id => - comments.byId[id].flagged === true && - comments.byId[id].status !== 'REJECTED' && - comments.byId[id].status !== 'ACCEPTED' - ); - const userActionIds = actions.ids.filter(id => actions.byId[id].item_type === 'USERS'); + const {data, moderation, settings, assets, ...props} = this.props; + const providedAssetId = this.props.params.id; + let asset; - // show the Pre-Mod tab if premod is enabled globally OR there are pre-mod comments in the db. - let enablePremodTab = (settings.settings && settings.settings.moderation === 'PRE') || premodIds.length; + if (data.loading) { + return
      ; + } + if (data.error) { + console.log(data); + return
      Error
      ; + } + + if (providedAssetId) { + asset = assets.find(asset => asset.id === this.props.params.id); + + if (!asset) { + return ; + } + } + + const enablePremodTab = !!data.premod.length; return ( - +
      + + + + +
      ); } } const mapStateToProps = state => ({ - comments: state.comments.toJS(), + moderation: state.moderation.toJS(), settings: state.settings.toJS(), - users: state.users.toJS(), - actions: state.actions.toJS(), + assets: state.assets.get('assets') }); -const mapDispatchToProps = dispatch => { - return { - fetchSettings: () => dispatch(fetchSettings()), - fetchModerationQueueComments: () => dispatch(fetchModerationQueueComments()), - fetchPremodQueue: () => dispatch(fetchPremodQueue()), - fetchRejectedQueue: () => dispatch(fetchRejectedQueue()), - fetchFlaggedQueue: () => dispatch(fetchFlaggedQueue()), - showBanUserDialog: (userId, userName, commentId) => dispatch(showBanUserDialog(userId, userName, commentId)), - hideBanUserDialog: () => dispatch(hideBanUserDialog(false)), - userStatusUpdate: (status, userId, commentId) => dispatch(userStatusUpdate(status, userId, commentId)).then(() => { - dispatch(fetchModerationQueueComments()); - }), - suspendUser: (userId, subject, text) => dispatch(userStatusUpdate('BANNED', userId)) - .then(() => dispatch(enableUsernameEdit(userId))) - .then(() => dispatch(sendNotificationEmail(userId, subject, text))) - .then(() => dispatch(fetchModerationQueueComments())) - , - updateStatus: (action, comment) => dispatch(updateStatus(action, comment)) - }; -}; +const mapDispatchToProps = dispatch => ({ + onTabClick: activeTab => dispatch(setActiveTab(activeTab)), + toggleModal: toggle => dispatch(toggleModal(toggle)), + onClose: () => dispatch(toggleModal(false)), + singleView: () => dispatch(singleView()), + updateAssets: assets => dispatch(updateAssets(assets)), + fetchSettings: () => dispatch(fetchSettings()), + showBanUserDialog: (user, commentId) => dispatch(showBanUserDialog(user, commentId)), + hideBanUserDialog: () => dispatch(hideBanUserDialog(false)), +}); -export default connect(mapStateToProps, mapDispatchToProps)(ModerationContainer); +export default compose( + connect(mapStateToProps, mapDispatchToProps), + setCommentStatus, + modQueueQuery, + banUser +)(ModerationContainer); diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.css b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.css deleted file mode 100644 index 84ee8b923..000000000 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.css +++ /dev/null @@ -1,53 +0,0 @@ -@custom-media --big-viewport (min-width: 780px); - -.listContainer { - max-width: 860px; - margin: 0 auto; -} - -.tabBar { - background: #262626; - 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; - } -} diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index 09b02daed..052138590 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -1,212 +1,34 @@ import React, {PropTypes} from 'react'; -import styles from './ModerationQueue.css'; -import ModerationKeysModal from 'components/ModerationKeysModal'; -import ModerationList from 'components/ModerationList'; -import BanUserDialog from 'components/BanUserDialog'; +import Comment from './components/Comment'; +import {actionsMap} from './helpers/moderationQueueActionsMap'; -import I18n from 'coral-framework/modules/i18n/i18n'; -import translations from '../../translations.json'; - -const lang = new I18n(translations); - -const ModerationQueue = (props) => ( -
      -
      -
      - { - e.preventDefault(); - props.onTabClick('all'); - }} - className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'all' ? styles.active : ''}`} - > - {lang.t('modqueue.all')} - - { - props.enablePremodTab - ? { - e.preventDefault(); - props.onTabClick('premod'); - }} - className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'premod' ? styles.active : ''}`}> - {lang.t('modqueue.premod')} - - : null - } - { - e.preventDefault(); - props.onTabClick('account'); - }} - className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'account' ? styles.active : ''}`}> - {lang.t('modqueue.account')} - - { - e.preventDefault(); - props.onTabClick('rejected'); - }} - className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'rejected' ? styles.active : ''}`} - > - {lang.t('modqueue.rejected')} - - { - e.preventDefault(); - props.onTabClick('flagged'); - }} - className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'flagged' ? styles.active : ''}`} - > - {lang.t('modqueue.flagged')} - -
      -
      - { - props.activeTab === 'all' && -
      - - -
      - } -
      +const ModerationQueue = props => { + return ( +
      +
        { - props.enablePremodTab - ?
        - { - props.activeTab === 'premod' && -
        - - -
        - } -
        - : null + props.data[props.activeTab].map((comment, i) => { + return ; + }) } - -
        - { - props.activeTab === 'account' && -
        - - -
        - } -
        -
        - { - props.activeTab === 'flagged' && -
        - - -
        - } -
        -
        - { - props.activeTab === 'rejected' && -
        - - -
        - } -
        - - +
      -
      -); + ); +}; ModerationQueue.propTypes = { - enablePremodTab: PropTypes.bool.isRequired + data: PropTypes.object.isRequired }; export default ModerationQueue; diff --git a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js new file mode 100644 index 000000000..ae70b303a --- /dev/null +++ b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js @@ -0,0 +1,79 @@ +import React from 'react'; +import timeago from 'timeago.js'; +import Linkify from 'react-linkify'; +import Highlighter from 'react-highlight-words'; +import {Link} from 'react-router'; + +import styles from './styles.css'; +import {Icon} from 'coral-ui'; +import ActionButton from '../../../components/ActionButton'; +import FlagBox from './FlagBox'; + +const linkify = new Linkify(); + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from 'coral-admin/src/translations.json'; +const lang = new I18n(translations); + +const Comment = ({actions = [], ...props}) => { + const links = linkify.getMatches(props.comment.body); + const actionSumaries = 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.comment.action_summaries ?

      {lang.t('comment.flagged')}

      : null} +
      +
      + {links ? Contains Link : null} +
      + {actions.map((action, i) => + props.acceptComment({commentId: props.comment.id})} + rejectComment={() => props.rejectComment({commentId: props.comment.id})} + showBanUserDialog={() => props.showBanUserDialog(props.comment.user, props.comment.id)} + /> + )} +
      + {props.comment.user.banned === 'banned' ? + + + {lang.t('comment.banned_user')} + + : null} +
      +
      + {!props.currentAsset && ( +
      + Article: {props.comment.asset.title} Moderate Article +
      + )} +
      +

      + + + +

      +
      + {actionSumaries && } + + {/* */} + {/* View context*/} + {/* */} +
    • + ); +}; + +const linkStyles = { + backgroundColor: 'rgb(255, 219, 135)', + padding: '1px 2px' +}; + +export default Comment; diff --git a/client/coral-admin/src/containers/ModerationQueue/components/FlagBox.js b/client/coral-admin/src/containers/ModerationQueue/components/FlagBox.js new file mode 100644 index 000000000..bf5a34f29 --- /dev/null +++ b/client/coral-admin/src/containers/ModerationQueue/components/FlagBox.js @@ -0,0 +1,19 @@ +import React, {PropTypes} from 'react'; +import styles from './styles.css'; + +const FlagBox = props => ( +
      +

      Flags:

      +
        + {props.actionSumaries.map((action, i) => +
      • {!action.reason ? No reason provided : action.reason} ({action.count})
      • + )} +
      +
      +); + +FlagBox.propTypes = { + actionSumaries: PropTypes.array.isRequired +}; + +export default FlagBox; diff --git a/client/coral-admin/src/containers/ModerationQueue/components/ModerationHeader.js b/client/coral-admin/src/containers/ModerationQueue/components/ModerationHeader.js new file mode 100644 index 000000000..474d8fd27 --- /dev/null +++ b/client/coral-admin/src/containers/ModerationQueue/components/ModerationHeader.js @@ -0,0 +1,25 @@ +import React from 'react'; +import {Link} from 'react-router'; +import styles from './styles.css'; + +const ModerationHeader = props => ( +
      +
      + { + props.asset ? +
      + All Streams + {props.asset.title} + Select Stream +
      + : +
      + + All Streams + Select Stream +
      + } +
      +
      +); +export default ModerationHeader; diff --git a/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js b/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js new file mode 100644 index 000000000..51605c443 --- /dev/null +++ b/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js @@ -0,0 +1,59 @@ +import React, {PropTypes} from 'react'; +import styles from './styles.css'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from 'coral-admin/src/translations.json'; + +const lang = new I18n(translations); + +const ModerationMenu = (props) => ( +
      +
      + { + e.preventDefault(); + props.onTabClick('all'); + }} + className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'all' ? styles.active : ''}`} + > + {lang.t('modqueue.all')} + + { + props.enablePremodTab + ? { + e.preventDefault(); + props.onTabClick('premod'); + }} + className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'premod' ? styles.active : ''}`}> + {lang.t('modqueue.premod')} + + : null + } + { + e.preventDefault(); + props.onTabClick('rejected'); + }} + className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'rejected' ? styles.active : ''}`} + > + {lang.t('modqueue.rejected')} + + { + e.preventDefault(); + props.onTabClick('flagged'); + }} + className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'flagged' ? styles.active : ''}`} + > + {lang.t('modqueue.flagged')} + +
      +
      +); + +ModerationMenu.propTypes = { + activeTab: PropTypes.string.isRequired, + enablePremodTab: PropTypes.bool +}; + +export default ModerationMenu; diff --git a/client/coral-admin/src/containers/ModerationQueue/components/NotFoundAsset.js b/client/coral-admin/src/containers/ModerationQueue/components/NotFoundAsset.js new file mode 100644 index 000000000..ffa1adfcd --- /dev/null +++ b/client/coral-admin/src/containers/ModerationQueue/components/NotFoundAsset.js @@ -0,0 +1,14 @@ +import React from 'react'; +import {Link} from 'react-router'; +import styles from './styles.css'; + +const NotFound = props => ( +
      +

      + The provided asset id {props.assetId} does not exist. + Go to Streams +

      +
      +); + +export default NotFound; diff --git a/client/coral-admin/src/containers/ModerationQueue/components/styles.css b/client/coral-admin/src/containers/ModerationQueue/components/styles.css new file mode 100644 index 000000000..658c9a059 --- /dev/null +++ b/client/coral-admin/src/containers/ModerationQueue/components/styles.css @@ -0,0 +1,327 @@ +@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; + + .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%; + margin: 0 auto; + display: flex; + justify-content: space-around; + width: 50%; + margin: 0; + } + + .actionButton { + transform: scale(1.4); + } + } +} + +.listItem { + border-bottom: 1px solid #e0e0e0; + font-size: 16px; + width: 100%; + max-width: 660px; + min-width: 400px; + margin: 0 auto; + padding: 16px 14px; + position: relative; + transition: box-shadow 200ms; + margin-top: 0; + + + &: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; + font-size: 0.88em; + color: black; + max-width: 500px; + word-wrap: break-word; + } + + .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 { + 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/helpers/moderationQueueActionsMap.js b/client/coral-admin/src/containers/ModerationQueue/helpers/moderationQueueActionsMap.js new file mode 100644 index 000000000..0da93e898 --- /dev/null +++ b/client/coral-admin/src/containers/ModerationQueue/helpers/moderationQueueActionsMap.js @@ -0,0 +1,13 @@ +export const actionsMap = { + PREMOD: ['REJECT', 'APPROVE', 'BAN'], + FLAGGED: ['REJECT', 'APPROVE'], + REJECTED: ['APPROVE'] +}; + +export const menuActionsMap = { + 'REJECT': {status: 'REJECTED', icon: 'close', key: 'r'}, + 'APPROVE': {status: 'ACCEPTED', icon: 'done', key: 't'}, + 'FLAGGED': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'}, + 'BAN': {status: 'BANNED', icon: 'not interested'}, + '': {icon: 'done'} +}; diff --git a/client/coral-admin/src/containers/Streams/Streams.css b/client/coral-admin/src/containers/Streams/Streams.css index 26fb4f74f..d8a75a4ce 100644 --- a/client/coral-admin/src/containers/Streams/Streams.css +++ b/client/coral-admin/src/containers/Streams/Streams.css @@ -55,6 +55,12 @@ border-left: none; border-right: none; + a { + color: rgb(44, 44, 44); + font-weight: 500; + text-decoration: none; + } + th { font-size: 1.1em; } diff --git a/client/coral-admin/src/containers/Streams/Streams.js b/client/coral-admin/src/containers/Streams/Streams.js index 1937f82c0..62f2f009e 100644 --- a/client/coral-admin/src/containers/Streams/Streams.js +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -4,14 +4,10 @@ import {connect} from 'react-redux'; import I18n from 'coral-framework/modules/i18n/i18n'; import {fetchAssets, updateAssetState} from '../../actions/assets'; import translations from '../../translations.json'; -import { - RadioGroup, - Radio, - Icon, - DataTable, - TableHeader -} from 'react-mdl'; -import Pager from 'coral-ui/components/Pager'; +import {Link} from 'react-router'; + +import {Pager, Icon} from 'coral-ui'; +import {DataTable, TableHeader, RadioGroup, Radio} from 'react-mdl'; const limit = 25; @@ -74,6 +70,8 @@ class Streams extends Component { } } + renderTitle = (title, {id}) => {title} + renderStatus = (closedAt, {id}) => { const closed = closedAt && new Date(closedAt).getTime() < Date.now(); const statusMenuOpen = this.state.statusMenus[id]; @@ -104,6 +102,9 @@ class Streams extends Component { render () { const {search, sort, filter} = this.state; const {assets} = this.props; + + const assetsIds = assets.ids.map((id) => assets.byId[id]); + return (
      @@ -142,16 +143,14 @@ class Streams extends Component {
      - assets.byId[id])}> - {lang.t('streams.article')} - - {lang.t('streams.pubdate')} - - - {lang.t('streams.status')} - + + {lang.t('streams.article')} + + {lang.t('streams.pubdate')} + + + {lang.t('streams.status')} + { assets: assets.toJS() }; }; + const mapDispatchToProps = (dispatch) => { return { fetchAssets: (...args) => { diff --git a/client/coral-admin/src/graphql/fragments/commentView.graphql b/client/coral-admin/src/graphql/fragments/commentView.graphql new file mode 100644 index 000000000..e78c28a28 --- /dev/null +++ b/client/coral-admin/src/graphql/fragments/commentView.graphql @@ -0,0 +1,15 @@ +fragment commentView on Comment { + id + body + created_at + status + user { + id + name: username + status + } + asset { + id + title + } +} diff --git a/client/coral-admin/src/graphql/mutations/index.js b/client/coral-admin/src/graphql/mutations/index.js new file mode 100644 index 000000000..fe3a1faf9 --- /dev/null +++ b/client/coral-admin/src/graphql/mutations/index.js @@ -0,0 +1,38 @@ +import {graphql} from 'react-apollo'; +import SET_USER_STATUS from './setUserStatus.graphql'; +import SET_COMMENT_STATUS from './setCommentStatus.graphql'; + +export const banUser = graphql(SET_USER_STATUS, { + props: ({mutate}) => ({ + banUser: ({userId}) => { + return mutate({ + variables: { + userId, + status: 'BANNED' + } + }); + }}), +}); + +export const setCommentStatus = graphql(SET_COMMENT_STATUS, { + props: ({mutate}) => ({ + acceptComment: ({commentId}) => { + return mutate({ + variables: { + commentId, + status: 'ACCEPTED' + }, + refetchQueries: ['ModQueue'] + }); + }, + rejectComment: ({commentId}) => { + return mutate({ + variables: { + commentId, + status: 'REJECTED' + }, + refetchQueries: ['ModQueue'] + }); + } + }) +}); diff --git a/client/coral-admin/src/graphql/mutations/setCommentStatus.graphql b/client/coral-admin/src/graphql/mutations/setCommentStatus.graphql new file mode 100644 index 000000000..7ff6173a8 --- /dev/null +++ b/client/coral-admin/src/graphql/mutations/setCommentStatus.graphql @@ -0,0 +1,7 @@ +mutation setCommentStatus($commentId: ID!, $status: COMMENT_STATUS!){ + setCommentStatus(id: $commentId, status: $status) { + errors { + translation_key + } + } +} diff --git a/client/coral-admin/src/graphql/mutations/setUserStatus.graphql b/client/coral-admin/src/graphql/mutations/setUserStatus.graphql new file mode 100644 index 000000000..32fcf7e20 --- /dev/null +++ b/client/coral-admin/src/graphql/mutations/setUserStatus.graphql @@ -0,0 +1,7 @@ +mutation setUserStatus($userId: ID!, $status: USER_STATUS!) { + setUserStatus(id: $userId, status: $status) { + errors { + translation_key + } + } +} diff --git a/client/coral-admin/src/graphql/queries/assetsQuery.graphql b/client/coral-admin/src/graphql/queries/assetsQuery.graphql new file mode 100644 index 000000000..37950692d --- /dev/null +++ b/client/coral-admin/src/graphql/queries/assetsQuery.graphql @@ -0,0 +1,6 @@ +query Assets { + assets { + id + title + } +} diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js new file mode 100644 index 000000000..fc59f8f84 --- /dev/null +++ b/client/coral-admin/src/graphql/queries/index.js @@ -0,0 +1,12 @@ +import {graphql} from 'react-apollo'; +import MOD_QUEUE_QUERY from './modQueueQuery.graphql'; + +export const modQueueQuery = graphql(MOD_QUEUE_QUERY, { + options: ({params: {id = ''}}) => { + return { + variables: { + asset_id: id + } + }; + } +}); diff --git a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql new file mode 100644 index 000000000..9eba7a971 --- /dev/null +++ b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql @@ -0,0 +1,38 @@ +#import "../fragments/commentView.graphql" + +query ModQueue ($asset_id: ID!) { + all: comments(query: { + statuses: [REJECTED, PREMOD], + asset_id: $asset_id + }) { + ...commentView + } + premod: comments(query: { + statuses: [PREMOD], + asset_id: $asset_id + }) { + ...commentView + } + flagged: comments(query: { + action_type: FLAG, + asset_id: $asset_id + }) { + ...commentView + action_summaries { + count + ... on FlagActionSummary { + reason + } + } + } + rejected: comments(query: { + statuses: [REJECTED], + asset_id: $asset_id + }) { + ...commentView + } + assets: assets { + id + title + } +} diff --git a/client/coral-admin/src/reducers/actions.js b/client/coral-admin/src/reducers/actions.js deleted file mode 100644 index 284e41c72..000000000 --- a/client/coral-admin/src/reducers/actions.js +++ /dev/null @@ -1,27 +0,0 @@ -import {Map, Set, fromJS} from 'immutable'; -import * as types from '../constants/actions'; - -const initialState = Map({ - ids: Set(), - byId: Map() -}); - -export default (state = initialState, action) => { - switch (action.type) { - case types.ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS: return addActions(state, action); - default: - return state; - } -}; - -const addActions = (state, action) => { - - // Make ids that are unique by item_id and by action type - const typeId = (action) => `${action.action_type}_${action.item_id}`; - const ids = action.actions.map(action => typeId(action)); - const map = action.actions.reduce((memo, action) => { - memo[typeId(action)] = action; - return memo; - }, {}); - return state.set('byId', fromJS(map)).set('ids', new Set(ids)); -}; diff --git a/client/coral-admin/src/reducers/assets.js b/client/coral-admin/src/reducers/assets.js index 77d0bf081..c9a82f1c5 100644 --- a/client/coral-admin/src/reducers/assets.js +++ b/client/coral-admin/src/reducers/assets.js @@ -1,20 +1,26 @@ import {Map, List, fromJS} from 'immutable'; -import {FETCH_ASSETS_SUCCESS, UPDATE_ASSET_STATE_REQUEST} from '../constants/assets'; +import * as actions from '../constants/assets'; const initialState = Map({ byId: Map(), - ids: List() + ids: List(), + assets: List() }); -export default (state = initialState, action) => { +export default function assets (state = initialState, action) { switch (action.type) { - case FETCH_ASSETS_SUCCESS: + case actions.FETCH_ASSETS_SUCCESS: return replaceAssets(action, state); - case UPDATE_ASSET_STATE_REQUEST: - return state.setIn(['byId', action.id, 'closedAt'], action.closedAt); - default: return state; + case actions.UPDATE_ASSET_STATE_REQUEST: + return state + .setIn(['byId', action.id, 'closedAt'], action.closedAt); + case actions.UPDATE_ASSETS: + return state + .set('assets', List(action.assets)); + default: + return state; } -}; +} const replaceAssets = (action, state) => { const assets = fromJS(action.assets.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {})); diff --git a/client/coral-admin/src/reducers/comments.js b/client/coral-admin/src/reducers/comments.js deleted file mode 100644 index 034016088..000000000 --- a/client/coral-admin/src/reducers/comments.js +++ /dev/null @@ -1,77 +0,0 @@ -import * as actions from '../constants/comments'; -import * as userActions from '../constants/users'; -import {Map, List, fromJS} from 'immutable'; - -/** - * Comments state is stored using 2 structures: - * - byId is a Map holding the comments using the item_id property as keys - * - ids is a List of item_id, this allows us to order and iterate easily - * since maps are unordered and some times we just need a list of things - */ - -const initialState = Map({ - byId: Map(), - ids: List(), - loading: false, - showBanUserDialog: false, - banUser: { - 'userName': '', - 'userId': '', - 'commentId': '' - } -}); - -// Handle the comment actions -export default (state = initialState, action) => { - switch (action.type) { - case actions.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST: return state.set('loading', true); - case actions.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS: return replaceComments(action, state); - case actions.COMMENTS_MODERATION_QUEUE_FAILED: return state.set('loading', false); - case actions.COMMENT_STATUS_UPDATE_REQUEST: return updateStatus(state, action); - case actions.COMMENT_FLAG: return flag(state, action); - case actions.COMMENT_CREATE_SUCCESS: return addComment(state, action); - case actions.COMMENT_STREAM_FETCH_SUCCESS: return replaceComments(action, state); - case actions.SHOW_BANUSER_DIALOG: return setBanUser(state, true, action); - case actions.HIDE_BANUSER_DIALOG: return setBanUser(state, false, action); - case actions.USER_BAN_SUCCESS: return setBanUser(state, false, action); - case userActions.UPDATE_STATUS_SUCCESS: return setBanUser(state, false, action); - default: return state; - } -}; - -// hide or show the UI for the dialog confirming the ban -// set the user that is going to set and the comment that is the reason -const setBanUser = (state, showBanUser, action) => { - const banUser = {'userName': action.userName, 'userId': action.userId, 'commentId': action.commentId}; - return state.set('showBanUserDialog', showBanUser) - .set('banUser', banUser); -}; - -// Update a comment status -const updateStatus = (state, action) => { - const byId = state.get('byId'); - const data = byId.get(action.id).set('status', action.status.toLowerCase()); - return state.set('byId', byId.set(action.id, data)); -}; - -// Flag a comment -const flag = (state, action) => { - const byId = state.get('byId'); - const data = byId.get(action.id).set('flagged', true); - const comment = byId.get(action.id).set('data', data); - return state.set('byId', byId.set(action.id, comment)); -}; - -// Replace the comment list with a new one -const replaceComments = (action, state) => { - const comments = fromJS(action.comments.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {})); - return state.set('byId', comments).set('loading', false) - .set('ids', List(comments.keys())); -}; - -// Add a new comment -const addComment = (state, action) => { - const comment = fromJS(action.comment); - return state.set('byId', state.get('byId').set(comment.get('item_id'), comment)) - .set('ids', state.get('ids').unshift(comment.get('item_id'))); -}; diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js index f12de96ba..e58f5bf5a 100644 --- a/client/coral-admin/src/reducers/index.js +++ b/client/coral-admin/src/reducers/index.js @@ -1,19 +1,13 @@ -import auth from 'reducers/auth'; -import users from 'reducers/users'; -import assets from 'reducers/assets'; -import actions from 'reducers/actions'; -import install from 'reducers/install'; -import comments from 'reducers/comments'; -import settings from 'reducers/settings'; -import community from 'reducers/community'; +import auth from './auth'; +import assets from './assets'; +import settings from './settings'; +import community from './community'; +import moderation from './moderation'; export default { - settings, - comments, - community, auth, - actions, assets, - users, - install + settings, + community, + moderation }; diff --git a/client/coral-admin/src/reducers/install.js b/client/coral-admin/src/reducers/install.js index a49f76b5e..596fd16cd 100644 --- a/client/coral-admin/src/reducers/install.js +++ b/client/coral-admin/src/reducers/install.js @@ -9,7 +9,7 @@ const initialState = Map({ organizationName: '' }), user: Map({ - displayName: '', + username: '', email: '', password: '', confirmPassword: '' @@ -17,7 +17,7 @@ const initialState = Map({ }), errors: Map({ organizationName: '', - displayName: '', + username: '', email: '', password: '', confirmPassword: '' diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js new file mode 100644 index 000000000..3131f6096 --- /dev/null +++ b/client/coral-admin/src/reducers/moderation.js @@ -0,0 +1,37 @@ +import {Map} from 'immutable'; +import * as actions from '../constants/moderation'; + +const initialState = Map({ + activeTab: 'all', + singleView: false, + modalOpen: false, + user: Map({}), + commentId: null, + banDialog: false +}); + +export default function moderation (state = initialState, action) { + switch (action.type) { + case actions.HIDE_BANUSER_DIALOG: + return state + .set('banDialog', false); + case actions.SHOW_BANUSER_DIALOG: + return state + .merge({ + user: Map(action.user), + commentId: action.commentId, + banDialog: true + }); + case actions.SET_ACTIVE_TAB: + return state + .set('activeTab', action.activeTab); + case actions.TOGGLE_MODAL: + return state + .set('modalOpen', action.open); + case actions.SINGLE_VIEW: + return state + .set('singleView', !state.get('singleView')); + default : + return state; + } +} diff --git a/client/coral-admin/src/reducers/settings.js b/client/coral-admin/src/reducers/settings.js index 4f743bc0a..70c52f028 100644 --- a/client/coral-admin/src/reducers/settings.js +++ b/client/coral-admin/src/reducers/settings.js @@ -1,5 +1,5 @@ import {Map, List} from 'immutable'; -import * as types from '../actions/settings'; +import * as actions from '../actions/settings'; const initialState = Map({ settings: Map({ @@ -16,48 +16,49 @@ const initialState = Map({ fetchingSettings: false }); -// Handle the comment actions -export default (state = initialState, action) => { +export default function settings (state = initialState, action) { switch (action.type) { - case types.SETTINGS_LOADING: return state.set('fetchingSettings', true).set('fetchSettingsError', null); - case types.SETTINGS_RECEIVED: return updateSettings(state, action); - case types.SETTINGS_FETCH_ERROR: return settingsFetchFailed(state, action); - case types.SETTINGS_UPDATED: return updateSettings(state, action); - case types.SAVE_SETTINGS_LOADING: return state.set('fetchingSettings', true).set('saveSettingsError', null); - case types.SAVE_SETTINGS_SUCCESS: return saveComplete(state, action); - case types.SAVE_SETTINGS_FAILED: return settingsSaveFailed(state, action); - case types.WORDLIST_UPDATED: return updateWordlist(state, action); - case types.DOMAINLIST_UPDATED: return updateDomainlist(state, action); - default: return state; + case actions.SETTINGS_LOADING: + return state + .set('fetchingSettings', true) + .set('fetchSettingsError', null); + case actions.SETTINGS_RECEIVED: + return state.merge({ + fetchingSettings: null, + fetchSettingsError: null, + ...action.settings + }); + case actions.SETTINGS_FETCH_ERROR: + return state + .set('fetchingSettings', false) + .set('fetchSettingsError', action.error); + case actions.SETTINGS_UPDATED: + return state.merge({ + fetchingSettings: null, + fetchSettingsError: null, + ...action.settings + }); + case actions.SAVE_SETTINGS_LOADING: + return state + .set('fetchingSettings', true) + .set('saveSettingsError', null); + case actions.SAVE_SETTINGS_SUCCESS: + return state.merge({ + fetchingSettings: false, + fetchSettingsError: null, + ...action.settings + }); + case actions.SAVE_SETTINGS_FAILED: + return state + .set('fetchingSettings', false) + .set('fetchSettingsError', action.error); + case actions.WORDLIST_UPDATED: + return state + .setIn(['settings', 'wordlist', action.listName], action.list); + case actions.DOMAINLIST_UPDATED: + return state + .setIn(['settings', 'domains', action.listName], action.list); + default: + return state; } -}; - -// only for updating top-level settings -const updateSettings = (state, action) => { - const s = state.set('fetchingSettings', false).set('fetchSettingsError', null); - const settings = s.get('settings').merge(action.settings); - return s.set('settings', settings); -}; - -// any nested settings must have a specialized setter -const updateWordlist = (state, action) => { - return state.setIn(['settings', 'wordlist', action.listName], action.list); -}; - -const updateDomainlist = (state, action) => { - return state.setIn(['settings', 'domains', action.listName], action.list); -}; - -const saveComplete = (state, action) => { - const s = state.set('fetchingSettings', false).set('saveSettingsError', null); - const settings = s.get('settings').merge(action.settings); - return s.set('settings', settings); -}; - -const settingsFetchFailed = (state, action) => { - return state.set('fetchingSettings', false).set('fetchSettingsError', action.error); -}; - -const settingsSaveFailed = (state, action) => { - return state.set('fetchingSettings', false).set('fetchSettingsError', action.error); -}; +} diff --git a/client/coral-admin/src/reducers/users.js b/client/coral-admin/src/reducers/users.js deleted file mode 100644 index ef589c155..000000000 --- a/client/coral-admin/src/reducers/users.js +++ /dev/null @@ -1,28 +0,0 @@ -import {Map, List, fromJS} from 'immutable'; - -const initialState = Map({ - byId: Map(), - ids: List() -}); - -export default (state = initialState, action) => { - switch (action.type) { - case 'USERS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceUsers(action, state); - case 'USER_STATUS_UPDATE': return updateUserStatus(state, action); - default: return state; - } -}; - -// Replace the comment list with a new one -const replaceUsers = (action, state) => { - const users = fromJS(action.users.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {})); - return state.set('byId', users) - .set('ids', List(users.keys())); -}; - -// Update a user status -const updateUserStatus = (state, action) => { - const byId = state.get('byId'); - const data = byId.get(action.author_id).set('status', action.status.toLowerCase()); - return state.set('byId', byId.set(action.author_id, data)); -}; diff --git a/client/coral-admin/src/services/PymConnection.js b/client/coral-admin/src/services/PymConnection.js new file mode 100644 index 000000000..ca592b824 --- /dev/null +++ b/client/coral-admin/src/services/PymConnection.js @@ -0,0 +1,9 @@ +import Pym from '../../node_modules/pym.js'; + +const pym = new Pym.Child({polling: 100}); +export default pym; + +export const link = (url) => (e) => { + e.preventDefault(); + pym.sendMessage('navigate', url); +}; diff --git a/client/coral-admin/src/services/client.js b/client/coral-admin/src/services/client.js index b4a7a38df..40a539634 100644 --- a/client/coral-admin/src/services/client.js +++ b/client/coral-admin/src/services/client.js @@ -2,7 +2,6 @@ import ApolloClient, {addTypename} from 'apollo-client'; import getNetworkInterface from './transport'; export const client = new ApolloClient({ - connectToDevTools: true, queryTransformer: addTypename, dataIdFromObject: (result) => { if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 0eb1ab98d..42028c3a2 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -198,7 +198,7 @@ }, "bandialog": { "ban_user": "Quieres suspender el Usuario?", - "are_you_sure": "Estas segura que quieres suspender a {props.author.displayName}?", + "are_you_sure": "Estas segura que quieres suspender a {props.author.username}?", "note": "Nota: Suspender este usuario también va a colocar este comentario en la cola de Rechazados.", "cancel": "Cancelar", "yes_ban_user": "Si, Suspendan el usuario" diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index 116c8d34e..e5eec37cb 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -16,6 +16,7 @@ import {Notification, notificationActions, authActions, assetActions, pym} from import Stream from './Stream'; import InfoBox from 'coral-plugin-infobox/InfoBox'; +import {ModerationLink} from 'coral-plugin-moderation'; import Count from 'coral-plugin-comment-count/CommentCount'; import CommentBox from 'coral-plugin-commentbox/CommentBox'; import UserBox from 'coral-sign-in/components/UserBox'; @@ -135,6 +136,7 @@ class Embed extends Component { charCount={asset.settings.charCountEnable && asset.settings.charCount} /> : null } +
      :

      {asset.settings.closedMessage}

      @@ -207,7 +209,7 @@ const mapDispatchToProps = dispatch => ({ }); }, clearNotification: () => dispatch(clearNotification()), - editName: (displayName) => dispatch(editName(displayName)), + editName: (username) => dispatch(editName(username)), showSignInDialog: (offset) => dispatch(showSignInDialog(offset)), logout: () => dispatch(logout()), dispatch: d => dispatch(d) diff --git a/client/coral-embed-stream/src/Stream.js b/client/coral-embed-stream/src/Stream.js index 1695cd578..6119cfc66 100644 --- a/client/coral-embed-stream/src/Stream.js +++ b/client/coral-embed-stream/src/Stream.js @@ -10,7 +10,7 @@ class Stream extends React.Component { asset: PropTypes.object.isRequired, comments: PropTypes.array.isRequired, currentUser: PropTypes.shape({ - displayName: PropTypes.string, + username: PropTypes.string, id: PropTypes.string }) } diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index bda46ae42..a2206fc09 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -15,11 +15,11 @@ export const hideCreateDisplayNameDialog = () => ({type: actions.HIDE_CREATEDISP const createDisplayNameSuccess = () => ({type: actions.CREATEDISPLAYNAME_SUCCESS}); const createDisplayNameFailure = error => ({type: actions.CREATEDISPLAYNAME_FAILURE, error}); -export const updateDisplayName = ({displayName}) => ({type: actions.UPDATE_DISPLAYNAME, displayName}); +export const updateDisplayName = ({username}) => ({type: actions.UPDATE_DISPLAYNAME, username}); export const createDisplayName = (userId, formData) => dispatch => { dispatch(createDisplayNameRequest()); - coralApi('/account/displayname', {method: 'PUT', body: formData}) + coralApi('/account/username', {method: 'PUT', body: formData}) .then(() => { dispatch(createDisplayNameSuccess()); dispatch(hideCreateDisplayNameDialog()); diff --git a/client/coral-framework/actions/user.js b/client/coral-framework/actions/user.js index 42d2e0398..bf2a77402 100644 --- a/client/coral-framework/actions/user.js +++ b/client/coral-framework/actions/user.js @@ -5,8 +5,8 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from './../translations'; const lang = new I18n(translations); -export const editName = (displayName) => (dispatch) => { - return coralApi('/account/displayname', {method: 'PUT', body: {displayName}}) +export const editName = (username) => (dispatch) => { + return coralApi('/account/username', {method: 'PUT', body: {username}}) .then(() => { dispatch(addNotification('success', lang.t('successNameUpdate'))); }); diff --git a/client/coral-framework/components/SuspendedAccount.js b/client/coral-framework/components/SuspendedAccount.js index 4c221aa81..00adc9952 100644 --- a/client/coral-framework/components/SuspendedAccount.js +++ b/client/coral-framework/components/SuspendedAccount.js @@ -14,16 +14,16 @@ class SuspendedAccount extends Component { } state = { - displayName: '', + username: '', alert: '' } onSubmitClick = (e) => { const {editName} = this.props; - const {displayName} = this.state; + const {username} = this.state; e.preventDefault(); - if (validate.displayName(displayName)) { - editName(displayName) + if (validate.username(username)) { + editName(username) .then(() => location.reload()) .catch((error) => { this.setState({alert: lang.t(`error.${error.message}`)}); @@ -36,7 +36,7 @@ class SuspendedAccount extends Component { render () { const {canEditName} = this.props; - const {displayName, alert} = this.state; + const {username, alert} = this.state; return
      { @@ -51,7 +51,7 @@ class SuspendedAccount extends Component { {alert}
      diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js index 03c083070..3a3404174 100644 --- a/client/coral-sign-in/components/SignUpContent.js +++ b/client/coral-sign-in/components/SignUpContent.js @@ -21,13 +21,13 @@ class SignUpContent extends React.Component { showErrors: PropTypes.bool, errors: PropTypes.shape({ email: PropTypes.string, - displayName: PropTypes.string, + username: PropTypes.string, password: PropTypes.string, confirmPassword: PropTypes.string, }), formData: PropTypes.shape({ email: PropTypes.string, - displayName: PropTypes.string, + username: PropTypes.string, password: PropTypes.string, confirmPassword: PropTypes.string }) @@ -89,12 +89,12 @@ class SignUpContent extends React.Component { onChange={handleChange} /> (
      {lang.t('signIn.loggedInAs')} - changeTab(1)}>{user.displayName}. {lang.t('signIn.notYou')} + changeTab(1)}>{user.username}. {lang.t('signIn.notYou')} {lang.t('signIn.logout')}
      ); diff --git a/client/coral-sign-in/containers/ChangeDisplayNameContainer.js b/client/coral-sign-in/containers/ChangeDisplayNameContainer.js index bc7fe8267..625985b63 100644 --- a/client/coral-sign-in/containers/ChangeDisplayNameContainer.js +++ b/client/coral-sign-in/containers/ChangeDisplayNameContainer.js @@ -21,7 +21,7 @@ import { class ChangeDisplayNameContainer extends Component { initialState = { formData: { - displayName: '', + username: '', }, errors: {}, showErrors: false diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index c92fe024d..fd9762ee3 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -29,7 +29,7 @@ class SignInContainer extends Component { initialState = { formData: { email: '', - displayName: '', + username: '', password: '', confirmPassword: '' }, diff --git a/client/coral-sign-in/translations.js b/client/coral-sign-in/translations.js index e8014014c..36ce3fce4 100644 --- a/client/coral-sign-in/translations.js +++ b/client/coral-sign-in/translations.js @@ -19,7 +19,7 @@ export default { register: 'Register', signUp: 'Sign Up', confirmPassword: 'Confirm Password', - displayName: 'Display Name', + username: 'Username', alreadyHaveAnAccount: 'Already have an account?', recoverPassword: 'Recover password', emailInUse: 'Email address already in use', @@ -32,10 +32,10 @@ export default { 'createdisplay': { writeyourusername: 'Write your username', yourusername: 'Your username is publicly visible on all comments you post. A username is needed before you can post your first comment.', - displayName: 'Display Name', + username: 'Username', save: 'Save', requiredField: 'Required field', - errorCreate: 'Error when changing display name', + errorCreate: 'Error when changing username', checkTheForm: 'Invalid Form. Please, check the fields', specialCharacters: 'Display names can contain letters, numbers and _ only' }, @@ -60,7 +60,7 @@ export default { register: 'Regístrate', signUp: 'Registro', confirmPassword: 'Confirmar Contraseña', - displayName: 'Nombre', + username: 'Nombre', alreadyHaveAnAccount: 'Ya tienes una cuenta?', recoverPassword: 'Recuperar contraseña', emailInUse: 'Este email se encuentra en uso', @@ -73,7 +73,7 @@ export default { 'createdisplay': { writeyourusername: 'Escribe tu nombre', yourusername: 'Tu nombre es visible publicamente en todos los comentarios que publiques. Es necesario tener un nombre de usuario antes de poder publicar tu primer comentario.', - displayName: 'Nombre a mostrar', + username: 'Nombre a mostrar', save: 'Guardar', requiredField: 'Campo necesario', errorCreate: 'Hubo un error al cambiar el nombre de usuario', diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6358f8f06..10e8f34f6 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -515,7 +515,7 @@ paths: - name: value in: query type: string - description: A term to search users' displayNames and email addresses. + description: A term to search users' usernames and email addresses. - name: sort in: query type: string @@ -576,7 +576,7 @@ paths: format: email password: type: string - displayName: + username: type: string responses: 201: @@ -899,7 +899,7 @@ definitions: id: type: string description: The uuid.v4 id of the user. - displayName: + username: type: string description: The name appearing next to the user's comments. disabled: diff --git a/errors.js b/errors.js index 7debe6a43..98fc6a52c 100644 --- a/errors.js +++ b/errors.js @@ -59,12 +59,12 @@ const ErrDisplayTaken = new APIError('Display name already in use', { status: 400 }); -const ErrSpecialChars = new APIError('No special characters are allowed in a display name', { +const ErrSpecialChars = new APIError('No special characters are allowed in a username', { translation_key: 'NO_SPECIAL_CHARACTERS', status: 400 }); -const ErrMissingDisplay = new APIError('A display name is required to create a user', { +const ErrMissingDisplay = new APIError('A username is required to create a user', { translation_key: 'DISPLAY_NAME_REQUIRED', status: 400 }); diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 6214c9df8..98891953c 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -162,22 +162,37 @@ const createPublicComment = (context, commentInput) => { })); }; +/** + * Sets the status of a comment + * @param {String} comment comment in graphql context + * @param {String} id identifier of the comment (uuid) + * @param {String} status the new status of the comment + */ + +const setCommentStatus = ({comment}, {id, status}) => { + return CommentsService.setStatus(id, status) + .then(res => res); +}; + module.exports = (context) => { // TODO: refactor to something that'll return an error in the event an attempt // is made to mutate state while not logged in. There's got to be a better way // to do this. - if (context.user && context.user.can('mutation:createComment')) { - return { - Comment: { - create: (comment) => createPublicComment(context, comment) - } - }; - } - - return { + let mutators = { Comment: { - create: () => Promise.reject(errors.ErrNotAuthorized) + create: () => Promise.reject(errors.ErrNotAuthorized), + setCommentStatus: () => Promise.reject(errors.ErrNotAuthorized) } }; + + if (context.user && context.user.can('mutation:createComment')) { + mutators.Comment.create = (comment) => createPublicComment(context, comment); + } + + if (context.user && context.user.can('mutation:setCommentStatus')) { + mutators.Comment.setCommentStatus = (action) => setCommentStatus(context, action); + } + + return mutators; }; diff --git a/graph/mutators/index.js b/graph/mutators/index.js index 58d0ed62c..b799cf83d 100644 --- a/graph/mutators/index.js +++ b/graph/mutators/index.js @@ -2,6 +2,7 @@ const _ = require('lodash'); const Comment = require('./comment'); const Action = require('./action'); +const User = require('./user'); module.exports = (context) => { @@ -9,6 +10,7 @@ module.exports = (context) => { return _.merge(...[ Comment, Action, + User, ].map((mutators) => { // Each set of mutators is a function which takes the context. diff --git a/graph/mutators/user.js b/graph/mutators/user.js new file mode 100644 index 000000000..2c43f11be --- /dev/null +++ b/graph/mutators/user.js @@ -0,0 +1,27 @@ +const errors = require('../../errors'); +const UsersService = require('../../services/users'); + +const setUserStatus = ({user}, {id, status}) => { + return UsersService.setStatus(id, status) + .then(res => res); +}; + +module.exports = (context) => { + + // TODO: refactor to something that'll return an error in the event an attempt + // is made to mutate state while not logged in. There's got to be a better way + // to do this. + if (context.user && context.user.can('mutation:setUserStatus')) { + return { + User: { + setUserStatus: (action) => setUserStatus(context, action) + } + }; + } + + return { + User: { + setUserStatus: () => Promise.reject(errors.ErrNotAuthorized) + } + }; +}; diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 8369b7281..4285f900d 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -27,6 +27,12 @@ const RootMutation = { deleteAction(_, {id}, {mutators: {Action}}) { return wrapResponse(null)(Action.delete({id})); }, + setUserStatus(_, {id, status}, {mutators: {User}}) { + return wrapResponse(null)(User.setUserStatus({id, status})); + }, + setCommentStatus(_, {id, status}, {mutators: {Comment}}) { + return wrapResponse(null)(Comment.setCommentStatus({id, status})); + } }; module.exports = RootMutation; diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 8aa14d5c6..ec3ac5b08 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -29,12 +29,7 @@ const RootQuery = { if (user != null && user.hasRoles('ADMIN') && action_type) { return Actions.getByTypes({action_type, item_type: 'COMMENTS'}) - .then((actions) => { - - // Map the actions from the items referenced byt this query. The actions - // returned by this query are explicitly going to be distinct by their - // `item_id`'s. - let ids = actions.map((action) => action.item_id); + .then((ids) => { // Perform the query using the available resolver. return Comments.getByQuery({ids, statuses, asset_id, parent_id, limit, cursor, sort}); diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index ddffcd708..b1c4824e8 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -27,8 +27,8 @@ type User { # The ID of the User. id: ID! - # display name of a user. - displayName: String! + # username of a user. + username: String! # Action summaries against the user. action_summaries: [ActionSummary] @@ -44,6 +44,9 @@ type User { # returns all comments based on a query. comments(query: CommentsQuery): [Comment] + + # returns user status + status: USER_STATUS } type Tag { @@ -360,6 +363,13 @@ enum SORT_ORDER { } # All queries that can be executed. +enum USER_STATUS { + ACTIVE + BANNED + PENDING + APPROVED +} + type RootQuery { # Site wide settings and defaults. @@ -466,6 +476,22 @@ type DeleteActionResponse implements Response { errors: [UserError] } +# SetUserStatusResponse is the response returned with possibly some errors +# relating to the delete action attempt. +type SetUserStatusResponse implements Response { + + # An array of errors relating to the mutation that occured. + errors: [UserError] +} + +# SetCommentStatusResponse is the response returned with possibly some errors +# relating to the delete action attempt. +type SetCommentStatusResponse implements Response { + + # An array of errors relating to the mutation that occured. + errors: [UserError] +} + # All mutations for the application are defined on this object. type RootMutation { @@ -480,6 +506,12 @@ type RootMutation { # Delete an action based on the action id. deleteAction(id: ID!): DeleteActionResponse + + # Sets User status + setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse + + # Sets Comment status + setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse } ################################################################################ diff --git a/models/user.js b/models/user.js index 06dc3c7d1..d56f030f3 100644 --- a/models/user.js +++ b/models/user.js @@ -12,7 +12,7 @@ const USER_STATUS = [ 'ACTIVE', 'BANNED', 'PENDING', - 'APPROVED' // Indicates that the users' displayname has been approved + 'APPROVED' // Indicates that the users' username has been approved ]; // ProfileSchema is the mongoose schema defined as the representation of a @@ -61,13 +61,20 @@ const UserSchema = new mongoose.Schema({ // This is sourced from the social provider or set manually during user setup // and simply provides a name to display for the given user. - displayName: { + username: { type: String, - unique: true, - lowercase: true, required: true }, + // TODO: find a way that we can instead utilize MongoDB 3.4's collation + // options to build the index in a case insenstive manner: + // https://docs.mongodb.com/manual/reference/collation/ + lowercaseUsername: { + type: String, + required: true, + unique: true + }, + // This is true when the user account is disabled, no action should be // acknowledged when they are disabled. Logins are also prevented. disabled: Boolean, @@ -147,7 +154,9 @@ const USER_GRAPH_OPERATIONS = [ 'mutation:createComment', 'mutation:createAction', 'mutation:deleteAction', - 'mutation:editName' + 'mutation:editName', + 'mutation:setUserStatus', + 'mutation:setCommentStatus' ]; /** @@ -163,6 +172,10 @@ UserSchema.method('can', function(...actions) { return false; } + if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:setCommentStatus') && !this.hasRoles('ADMIN')) { + return false; + } + return true; }); diff --git a/routes/api/account/index.js b/routes/api/account/index.js index d383e9f36..3de9d41c3 100644 --- a/routes/api/account/index.js +++ b/routes/api/account/index.js @@ -115,9 +115,9 @@ router.put('/password/reset', (req, res, next) => { }); }); -router.put('/displayname', authorization.needed(), (req, res, next) => { +router.put('/username', authorization.needed(), (req, res, next) => { UsersService - .editName(req.user.id, req.body.displayName) + .editName(req.user.id, req.body.username) .then(() => { res.status(204).end(); }) diff --git a/routes/api/auth/index.js b/routes/api/auth/index.js index e3acf2c77..dcce15933 100644 --- a/routes/api/auth/index.js +++ b/routes/api/auth/index.js @@ -70,7 +70,7 @@ const HandleAuthPopupCallback = (req, res, next) => (err, user) => { return res.render('auth-callback', {err: JSON.stringify(errors.ErrNotAuthorized), data: null}); } - // Authorize the user to edit their displayName. + // Authorize the user to edit their username. UsersService.toggleNameEdit(user.id, true) .then(() => { diff --git a/routes/api/setup/index.js b/routes/api/setup/index.js index 60f7d7fd2..e325769cd 100644 --- a/routes/api/setup/index.js +++ b/routes/api/setup/index.js @@ -32,11 +32,11 @@ router.post('/', (req, res, next) => { const { settings, - user: {email, password, displayName} + user: {email, password, username} } = req.body; SetupService - .setup({settings, user: {email, password, displayName}}) + .setup({settings, user: {email, password, username}}) .then(() => { // We're setup! diff --git a/routes/api/users/index.js b/routes/api/users/index.js index 1ce429cb4..4182beb49 100644 --- a/routes/api/users/index.js +++ b/routes/api/users/index.js @@ -120,11 +120,11 @@ const SendEmailConfirmation = (app, userID, email, referer) => UsersService // create a local user. router.post('/', (req, res, next) => { - const {email, password, displayName} = req.body; + const {email, password, username} = req.body; const redirectUri = req.header('X-Pym-Url') || req.header('Referer'); UsersService - .createLocalUser(email, password, displayName) + .createLocalUser(email, password, username) .then((user) => { // Send an email confirmation. The Front end will know about the diff --git a/scripts/pree2e.sh b/scripts/pree2e.sh index da308f738..5b6f21ab3 100755 --- a/scripts/pree2e.sh +++ b/scripts/pree2e.sh @@ -1,5 +1,8 @@ #!/bin/bash +# fail the e2e if any of these fail +set -e + # install selenium selenium-standalone install --config=./selenium.config.js diff --git a/services/comments.js b/services/comments.js index fc05341cb..bcc534c51 100644 --- a/services/comments.js +++ b/services/comments.js @@ -7,6 +7,12 @@ const ALLOWED_TAGS = [ {name: 'STAFF'} ]; +const STATUSES = [ + 'ACCEPTED', + 'REJECTED', + 'PREMOD', +]; + module.exports = class CommentsService { /** @@ -249,4 +255,25 @@ module.exports = class CommentsService { return CommentModel.find(query); } + + /** + * Sets Comment Status + * @param {String} id identifier of the comment (uuid) + * @param {String} status the new status of the comment + * @return {Promise} + */ + + static setStatus(id, status) { + + // Check to see if the comment status is in the allowable set of statuses. + if (STATUSES.indexOf(status) === -1) { + + // Comment status is not supported! Error out here. + return Promise.reject(new Error(`status ${status} is not supported`)); + } + + return CommentModel.update({id}, { + $set: {status} + }); + } }; diff --git a/services/passport.js b/services/passport.js index dcb6904ec..d265d9812 100644 --- a/services/passport.js +++ b/services/passport.js @@ -102,6 +102,9 @@ if (process.env.TALK_FACEBOOK_APP_ID && process.env.TALK_FACEBOOK_APP_SECRET && clientID: process.env.TALK_FACEBOOK_APP_ID, clientSecret: process.env.TALK_FACEBOOK_APP_SECRET, callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`, + + // TODO: remove displayName reference when we have steps in the FE to handle + // the username create flow. profileFields: ['id', 'displayName', 'picture.type(large)'] }, (accessToken, refreshToken, profile, done) => { UsersService diff --git a/services/setup.js b/services/setup.js index 74ef1d431..2bfbc66a0 100644 --- a/services/setup.js +++ b/services/setup.js @@ -45,7 +45,7 @@ module.exports = class SetupService { /** * This verifies that the current input for the setup is valid. */ - static validate({settings, user: {email, displayName, password}}) { + static validate({settings, user: {email, username, password}}) { // Verify the email address of the user. if (!email) { @@ -57,7 +57,7 @@ module.exports = class SetupService { // Verify other properties of the user. return Promise.all([ - UsersService.isValidDisplayName(displayName, false), + UsersService.isValidDisplayName(username, false), UsersService.isValidPassword(password), settingsModel.validate() ]); @@ -66,11 +66,11 @@ module.exports = class SetupService { /** * This will perform the setup. */ - static setup({settings, user: {email, password, displayName}}) { + static setup({settings, user: {email, password, username}}) { // Validate the settings first. return SetupService - .validate({settings, user: {email, password, displayName}}) + .validate({settings, user: {email, password, username}}) .then(() => { return SettingsService.update(settings); }) @@ -80,7 +80,7 @@ module.exports = class SetupService { // Create the user. return UsersService - .createLocalUser(email, password, displayName) + .createLocalUser(email, password, username) // Grant them administrative privileges and confirm the email account. .then((user) => { diff --git a/services/users.js b/services/users.js index 408d5c618..1b8045c84 100644 --- a/services/users.js +++ b/services/users.js @@ -99,19 +99,23 @@ module.exports = class UsersService { .then(() => dstUser.save()); } + static castDisplayName(displayName) { + return displayName.replace(/ /g, '_').replace(/[^a-zA-Z_]/g, ''); + } + /** * Finds a user given a social profile and if the user does not exist, creates * them. * @param {Object} profile - User social/external profile * @param {Function} done [description] */ - static findOrCreateExternalUser(profile) { + static findOrCreateExternalUser({id, provider, displayName}) { return UserModel .findOne({ profiles: { $elemMatch: { - id: profile.id, - provider: profile.provider + id, + provider } } }) @@ -120,16 +124,16 @@ module.exports = class UsersService { return user; } + // TODO: remove displayName reference when we have steps in the FE to handle + // the username create flow. + let username = UsersService.castDisplayName(displayName); + // The user was not found, lets create them! user = new UserModel({ - displayName: profile.displayName, + username, + lowercaseUsername: username.toLowerCase(), roles: [], - profiles: [ - { - id: profile.id, - provider: profile.provider - } - ] + profiles: [{id, provider}] }); return user.save(); @@ -164,24 +168,24 @@ module.exports = class UsersService { static createLocalUsers(users) { return Promise.all(users.map((user) => { return UsersService - .createLocalUser(user.email, user.password, user.displayName); + .createLocalUser(user.email, user.password, user.username); })); } /** - * Check the requested displayname for naughty words (currently in English) and special chars - * @param {String} displayName word to be checked for profanity + * Check the requested username for blocked words and special chars + * @param {String} username word to be checked for profanity * @param {Boolean} checkAgainstWordlist enables cheching against the wordlist - * @return {Promise} rejected if the machine's sensibilites are offended + * @return {Promise} */ - static isValidDisplayName(displayName, checkAgainstWordlist = true) { + static isValidDisplayName(username, checkAgainstWordlist = true) { const onlyLettersNumbersUnderscore = /^[A-Za-z0-9_]+$/; - if (!displayName) { + if (!username) { return Promise.reject(errors.ErrMissingDisplay); } - if (!onlyLettersNumbersUnderscore.test(displayName)) { + if (!onlyLettersNumbersUnderscore.test(username)) { return Promise.reject(errors.ErrSpecialChars); } @@ -189,11 +193,11 @@ module.exports = class UsersService { if (checkAgainstWordlist) { // check for profanity - return Wordlist.displayNameCheck(displayName); + return Wordlist.usernameCheck(username); } // No errors found! - return Promise.resolve(displayName); + return Promise.resolve(username); } /** @@ -215,23 +219,23 @@ module.exports = class UsersService { * Creates the local user with a given email, password, and name. * @param {String} email email of the new user * @param {String} password plaintext password of the new user - * @param {String} displayName name of the display user + * @param {String} username name of the display user * @param {Function} done callback */ - static createLocalUser(email, password, displayName) { + static createLocalUser(email, password, username) { if (!email) { return Promise.reject(errors.ErrMissingEmail); } email = email.toLowerCase().trim(); - displayName = displayName.toLowerCase().trim(); + username = username.trim(); return Promise.all([ - UsersService.isValidDisplayName(displayName), + UsersService.isValidDisplayName(username), UsersService.isValidPassword(password) ]) - .then(() => { // displayName is valid + .then(() => { // username is valid return new Promise((resolve, reject) => { bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => { if (err) { @@ -239,7 +243,8 @@ module.exports = class UsersService { } let user = new UserModel({ - displayName: displayName, + username, + lowercaseUsername: username.toLowerCase(), password: hashedPassword, roles: [], profiles: [ @@ -253,7 +258,7 @@ module.exports = class UsersService { user.save((err) => { if (err) { if (err.code === 11000) { - if (err.message.match('displayName')) { + if (err.message.match('username')) { return reject(errors.ErrDisplayTaken); } return reject(errors.ErrEmailTaken); @@ -397,7 +402,7 @@ module.exports = class UsersService { static findPublicByIdArray(ids) { return UserModel.find({ id: {$in: ids} - }, 'id displayName'); + }, 'id username'); } /** @@ -473,7 +478,7 @@ module.exports = class UsersService { /** * Finds a user using a value which gets compared using a prefix match against - * the user's email address and/or their display name. + * the user's email address and/or their username. * @param {String} value value to search by * @return {Promise} */ @@ -481,9 +486,9 @@ module.exports = class UsersService { return UserModel.find({ $or: [ - // Search by a prefix match on the displayName. + // Search by a prefix match on the username. { - 'displayName': { + 'username': { $regex: new RegExp(`^${value}`), $options: 'i' } @@ -667,18 +672,19 @@ module.exports = class UsersService { } /** - * Updates the user's displayName. + * Updates the user's username. * @param {String} id the id of the user to be enabled. - * @param {String} displayName The new displayname for the user. + * @param {String} username The new username for the user. * @return {Promise} */ - static editName(id, displayName) { + static editName(id, username) { return UserModel.update({ id, canEditName: true }, { $set: { - displayName: displayName.toLowerCase(), + username: username, + lowercaseUsername: username.toLowerCase(), canEditName: false, status: 'PENDING' } diff --git a/services/wordlist.js b/services/wordlist.js index dde032f97..8dc16fba2 100644 --- a/services/wordlist.js +++ b/services/wordlist.js @@ -201,22 +201,22 @@ class Wordlist { /** * check potential username for banned words, special characters */ - static displayNameCheck(displayName) { + static usernameCheck(username) { const wl = new Wordlist(); return wl.load() .then(() => { - displayName = displayName.replace(/_/g, ''); + username = username.replace(/_/g, ''); // test each word, and fail if we find a match const hasBadWords = wl.lists.banned.some(phrase => { - return displayName.indexOf(phrase.join('')) !== -1; + return username.indexOf(phrase.join('')) !== -1; }); if (hasBadWords) { throw Errors.ErrContainsProfanity; } else { - return Promise.resolve(displayName); + return Promise.resolve(username); } }); } diff --git a/test/e2e/pages/embedStreamPage.js b/test/e2e/pages/embedStreamPage.js index 1dd467b10..28a9929ea 100644 --- a/test/e2e/pages/embedStreamPage.js +++ b/test/e2e/pages/embedStreamPage.js @@ -19,7 +19,7 @@ const embedStreamCommands = { .setValue('@signInDialogEmail', user.email) .setValue('@signInDialogPassword', user.pass) .setValue('@signUpDialogConfirmPassword', user.pass) - .setValue('@signUpDialogDisplayName', user.displayName) + .setValue('@signUpDialogDisplayName', user.username) .waitForElementVisible('@signUpButton') .click('@signUpButton') .waitForElementVisible('@signInViewTrigger') @@ -96,7 +96,7 @@ module.exports = { selector: '#signInDialog #confirmPassword' }, signUpDialogDisplayName: { - selector: '#signInDialog #displayName' + selector: '#signInDialog #username' }, logInButton: { selector: '#coralLogInButton' diff --git a/test/e2e/tests/EmbedStreamTests.js b/test/e2e/tests/EmbedStreamTests.js index 1b8172ef5..40429cb78 100644 --- a/test/e2e/tests/EmbedStreamTests.js +++ b/test/e2e/tests/EmbedStreamTests.js @@ -37,7 +37,7 @@ module.exports = { .click('#coralRegister') .waitForElementVisible('#email', 1000) .setValue('#email', mockUser.email) - .setValue('#displayName', mockUser.name) + .setValue('#username', mockUser.name) .setValue('#password', mockUser.pw) .setValue('#confirmPassword', mockUser.pw) .click('#coralSignUpButton') @@ -125,7 +125,7 @@ module.exports = { // Add a mock user .then(() => mocks.users([{ - displayName: 'BabyBlue', + username: 'BabyBlue', email: 'whale@tale.sea', password: 'krillaretasty' }])) diff --git a/test/e2e/tests/Visitor/SignUpTest.js b/test/e2e/tests/Visitor/SignUpTest.js index d19ca1d8c..253b41659 100644 --- a/test/e2e/tests/Visitor/SignUpTest.js +++ b/test/e2e/tests/Visitor/SignUpTest.js @@ -13,7 +13,7 @@ module.exports = { embedStreamPage .signUp({ email: `visitor_${Date.now()}@test.com`, - displayName: `visitor${Date.now()}`, + username: `visitor${Date.now()}`, pass: 'testtest' }); }, diff --git a/test/routes/api/account/index.js b/test/routes/api/account/index.js index 5b9acfa9d..3a4493279 100644 --- a/test/routes/api/account/index.js +++ b/test/routes/api/account/index.js @@ -13,7 +13,7 @@ chai.use(require('chai-http')); const UsersService = require('../../../../services/users'); -describe('/api/v1/account/displayname', () => { +describe('/api/v1/account/username', () => { let mockUser; beforeEach(() => SettingsService.init(settings).then(() => { @@ -29,9 +29,9 @@ describe('/api/v1/account/displayname', () => { .post(`/api/v1/users/${mockUser.id}/username-enable`) .set(passport.inject({id: '456', roles: ['ADMIN']})) .then(() => chai.request(app) - .put('/api/v1/account/displayname') + .put('/api/v1/account/username') .set(passport.inject({id: mockUser.id, roles: []})) - .send({displayName: 'MojoJojo'})) + .send({username: 'MojoJojo'})) .then((res) => { expect(res).to.have.status(204); }); @@ -42,9 +42,9 @@ describe('/api/v1/account/displayname', () => { .post(`/api/v1/users/${mockUser.id}/username-enable`) .set(passport.inject({id: '456', roles: ['ADMIN']})) .then(() => chai.request(app) - .put('/api/v1/account/displayname') + .put('/api/v1/account/username') .set(passport.inject({id: 'wrongid', roles: []})) - .send({displayName: 'MojoJojo'})) + .send({username: 'MojoJojo'})) .then(() => { done(new Error('Exected Error')); }) @@ -56,7 +56,7 @@ describe('/api/v1/account/displayname', () => { it('it should return an error when the user tries to edit their username if canEditName is disabled', (done) => { chai.request(app) - .put('/api/v1/account/displayname') + .put('/api/v1/account/username') .set(passport.inject({id: mockUser.id, roles: []})) .send({username: 'MojoJojo'}) .then(() => { diff --git a/test/routes/api/auth/index.js b/test/routes/api/auth/index.js index 9f3937a91..38b867b7d 100644 --- a/test/routes/api/auth/index.js +++ b/test/routes/api/auth/index.js @@ -45,7 +45,7 @@ describe('/api/v1/auth/local', () => { expect(res2).to.have.status(200); expect(res2).to.be.json; expect(res2.body).to.have.property('user'); - expect(res2.body.user).to.have.property('displayName', 'maria'); + expect(res2.body.user).to.have.property('username', 'Maria'); }); }); @@ -91,7 +91,7 @@ describe('/api/v1/auth/local', () => { expect(res).to.have.status(200); expect(res).to.be.json; expect(res.body).to.have.property('user'); - expect(res.body.user).to.have.property('displayName', 'maria'); + expect(res.body.user).to.have.property('username', 'Maria'); }); }); }); diff --git a/test/routes/api/comments/index.js b/test/routes/api/comments/index.js index 0ee19fc5e..50431159b 100644 --- a/test/routes/api/comments/index.js +++ b/test/routes/api/comments/index.js @@ -49,11 +49,11 @@ describe('/api/v1/comments', () => { }]; const users = [{ - displayName: 'Ana', + username: 'Ana', email: 'ana@gmail.com', password: '123456789' }, { - displayName: 'Maria', + username: 'Maria', email: 'maria@gmail.com', password: '123456789' }]; @@ -183,11 +183,11 @@ describe('/api/v1/comments/:comment_id', () => { }]; const users = [{ - displayName: 'Ana', + username: 'Ana', email: 'ana@gmail.com', password: '123456789' }, { - displayName: 'Maria', + username: 'Maria', email: 'maria@gmail.com', password: '123456789' }]; diff --git a/test/routes/api/queue/index.js b/test/routes/api/queue/index.js index 9f89a961e..bca68f355 100644 --- a/test/routes/api/queue/index.js +++ b/test/routes/api/queue/index.js @@ -45,11 +45,11 @@ describe('/api/v1/queue', () => { }]; const users = [{ - displayName: 'Ana', + username: 'Ana', email: 'ana@gmail.com', password: '123456789' }, { - displayName: 'Maria', + username: 'Maria', email: 'maria@gmail.com', password: '123456789' }]; @@ -103,7 +103,7 @@ describe('/api/v1/queue', () => { expect(res).to.have.status(200); expect(res.body.comments).to.have.length(1); expect(res.body.comments[0]).to.have.property('body'); - expect(res.body.users[0]).to.have.property('displayName'); + expect(res.body.users[0]).to.have.property('username'); expect(res.body.actions[0]).to.have.property('action_type'); }); }); @@ -115,7 +115,7 @@ describe('/api/v1/queue', () => { .end(function(err, res){ expect(err).to.be.null; expect(res).to.have.status(200); - expect(res.body.users[0]).to.have.property('displayName'); + expect(res.body.users[0]).to.have.property('username'); expect(res.body.actions[0]).to.have.property('action_type'); done(); }); diff --git a/test/services/comments.js b/test/services/comments.js index c27050ae7..4c3e0eab9 100644 --- a/test/services/comments.js +++ b/test/services/comments.js @@ -69,15 +69,12 @@ describe('services.CommentsService', () => { const users = [{ email: 'stampi@gmail.com', - displayName: 'Stampi', - password: '1Coral!!', - roles: ['ADMIN'], - _id: '1' + username: 'Stampi', + password: '1Coral!!' }, { email: 'sockmonster@gmail.com', - displayName: 'Sockmonster', - password: '2Coral!!', - _id : '2' + username: 'Sockmonster', + password: '2Coral!!' }]; const actions = [{ @@ -257,42 +254,5 @@ describe('services.CommentsService', () => { expect(c.status_history[1]).to.have.property('assigned_by', '123'); }); }); - - }); - - describe('#tagByStaff()', () => { - - it('creates a new comment by admin', () => { - return UsersService.findLocalUser('stampi@gmail.com', '1Coral!!').then((user) => { - return UsersService.addRoleToUser(user.id, 'ADMIN').then(() => { - UsersService.findById(user.id).then((u) => { - return CommentsService - .publicCreate({ - body: 'This is a comment!', - status: 'ACCEPTED', - author_id: u.id - }).then((c) => { - expect(c).to.not.be.null; - expect(c.tags).to.not.have.length(0); - expect(c.tags[0].name).to.be.equal('STAFF'); - }); - }); - }); - }); - }); - - it('creates a new comment by non admin', () => { - return UsersService.findLocalUser('sockmonster@gmail.com', '2Coral!!').then((user) => { - return CommentsService - .publicCreate({ - body: 'This is a comment!', - status: 'ACCEPTED', - author_id: user.id - }).then((c) => { - expect(c).to.not.be.null; - expect(c.tags).to.have.length(0); - }); - }); - }); }); }); diff --git a/test/services/users.js b/test/services/users.js index 5774d45ae..51ba01403 100644 --- a/test/services/users.js +++ b/test/services/users.js @@ -12,15 +12,15 @@ describe('services.UsersService', () => { return SettingsService.init(settings).then(() => { return UsersService.createLocalUsers([{ email: 'stampi@gmail.com', - displayName: 'Stampi', + username: 'Stampi', password: '1Coral!-' }, { email: 'sockmonster@gmail.com', - displayName: 'Sockmonster', + username: 'Sockmonster', password: '2Coral!2' }, { email: 'marvel@gmail.com', - displayName: 'Marvel', + username: 'Marvel', password: '3Coral!3' }]).then((users) => { mockUsers = users; @@ -33,7 +33,7 @@ describe('services.UsersService', () => { return UsersService .findById(mockUsers[0].id) .then((user) => { - expect(user).to.have.property('displayName', 'stampi'); + expect(user).to.have.property('username', 'Stampi'); }); }); }); @@ -53,11 +53,11 @@ describe('services.UsersService', () => { return UsersService.findPublicByIdArray(ids).then((result) => { expect(result).to.have.length(3); const sorted = result.sort((a, b) => { - if(a.displayName < b.displayName) {return -1;} - if(a.displayName > b.displayName) {return 1;} + if(a.username < b.username) {return -1;} + if(a.username > b.username) {return 1;} return 0; }); - expect(sorted[0]).to.have.property('displayName', 'marvel'); + expect(sorted[0]).to.have.property('username', 'Marvel'); }); }); }); @@ -68,8 +68,7 @@ describe('services.UsersService', () => { return UsersService .findLocalUser(mockUsers[0].profiles[0].id, '1Coral!-') .then((user) => { - expect(user).to.have.property('displayName') - .and.to.equal(mockUsers[0].displayName.toLowerCase()); + expect(user).to.have.property('username', mockUsers[0].username); }); }); @@ -84,10 +83,10 @@ describe('services.UsersService', () => { }); describe('#createLocalUser', () => { - it('should not create a user with duplicate display name', () => { + it('should not create a user with duplicate username', () => { return UsersService.createLocalUsers([{ email: 'otrostampi@gmail.com', - displayName: 'StampiTheSecond', + username: 'StampiTheSecond', password: '1Coralito!' }]) .then((user) => { @@ -227,7 +226,7 @@ describe('services.UsersService', () => { .then(() => UsersService.editName(mockUsers[0].id, 'Jojo')) .then(() => UsersService.findById(mockUsers[0].id)) .then((user) => { - expect(user).to.have.property('displayName', 'jojo'); + expect(user).to.have.property('username', 'Jojo'); expect(user).to.have.property('canEditName', false); }); }); diff --git a/views/auth-callback.ejs b/views/auth-callback.ejs index d38d759ee..66b9cfede 100644 --- a/views/auth-callback.ejs +++ b/views/auth-callback.ejs @@ -3,7 +3,7 @@