diff --git a/.gitignore b/.gitignore index c76651868..2ec55accd 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ dump.rdb .env gaba.cfg .idea/ +coverage/ +yarn.lock diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9fa7985c0..66ae8ade8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,95 +1,78 @@ -# Contribution Guide +# Contributor's Guide -We're very excited that you're interested in contributing to Talk! There is much to do. Before you begin, please review this document to get a sense of the practices and philosophies that hold this project together. +Welcome! We are very excited that you are interested in contributing to Talk. + +This document is a companion to help you approach contributing. If it does not do so, please [let us know how we can improve it](https://github.com/coralproject/talk/issues)! -## Doing the Work +## Product Roadmap -We are here to make it as seamless as possible to contribute to Talk. The following lists are meant to make it straightforward to perform the mechanics of working on the project so you can focus your energy toward writing and reviewing content. +You can view what the Coral Team is working on next here https://www.pivotaltracker.com/n/projects/1863625. + +You can view product ideas and our longer term roadmap here https://trello.com/b/ILND751a/talk. -### Code Reviews +## Contribute to the documentation +Clear docs are a prerequisite for a successful open source project. We value non-code and code contributions equally. -One of the most valuable aspects of working in software. It is something that should challenge the reviewer and author alike. It is a way of focusing knowledge, experience and opinions for the benefit of the project and the participants. +We are looking for _documentarians_ to: -Code reviews are a collaboration to make _the work_ as good as it can be. Code reviews are not a good venue for providing direct instruction to _the author._ Focus on positive, incremental improvements that can be made on the work at hand. +* make clarity, grammar and completeness updates, +* create new / missing sections, and +* take the lead in making sections, or the over all structure better. -Please take your time when writing and reviewing code. Here are some fundamental questions to open up a reviewing headspace. +### But how? -**Is the code clear, efficient and a pleasure to read?** +* Our public docs site can be updated [here](https://github.com/coralproject/docs). +* [Let us know](https://github.com/coralproject/talk/wiki/Contact-Us) if you'd like permission to update our wiki. +* Update any of our .md docs files by following [this guide](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging). +* Our API docs need to be kept honest. [Update them here](https://github.com/coralproject/talk/blob/master/docs/swagger.yaml). -Somewhere at the intersection of good variable names, well laid out file structures, consistent formatting and appropriate comments lies beautiful code. Code is language spoken to at least two very distinct audiences, the computer that interprets it and the developer who encounters it. Both should be at the front of your mind when reviewing code. +## Integrate into your environment -Thinking like a computer, you could ask: +First, [set up a dev environment](https://github.com/coralproject/talk/blob/master/INSTALL.md). Please let us know how this goes! -* Is the code using memory efficiently? -* Is data being moved around unnecessarily? -* Are multiple network requests being made where fewer would do? -* Is there excess processing happening in a synchronous flow that may disrupt user experience? -* Are there large libraries included for small gains? +Talk is designed to integrate into existing environments in a variety of ways: -Then, returning to your human roots... Is the code readable? +* [Auth integrations](https://github.com/coralproject/talk/wiki/Security#authentication-strategies) +* [Push assets into Talk](https://github.com/coralproject/talk/blob/master/routes/api/assets/index.js) +* Monitoring Hooks (coming in 2017) -* Can I understand what is happening here (and maybe even why) by simply opening up the file, starting at the top and reading downward? -* Do comments convey clear, full thoughts in a narrative language that provides background for the code choices? -* Are the files separated logically such that each one contains a clear concept of code? +If you're considering deploying Talk, [please let us know](https://github.com/coralproject/talk/wiki/Contact-Us)! We are quite literally doing this for you and want to help you succeed any way we can. + +If you are writing custom integration code in your fork of Talk, please consider keeping it generic and filing a Pull Request to contribute it back to the project! See our [forking and merging guidelines](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging) for more info. + +## Write some code + +First, [set up a dev environment](https://github.com/coralproject/talk/blob/master/INSTALL.md). Please let us know how this goes! + +### Build a New Feature / Plugin + +Talk is beginning life as a Commenting Platform, but is architected to support many varieties of community engagement. + +Please [contact us](https://github.com/coralproject/talk/wiki/Contact-Us) early and often if you'd like to help. We would love to hear your ideas for features and plugins and help you find a way to productively engage the project. + +To get an idea of where the Coral Team is going, see: + +* our [product/design Trello board](https://trello.com/b/ILND751a/talk), +* our [current stories](https://www.pivotaltracker.com/n/projects/1863625), and +* our [issues](https://github.com/coralproject/talk/issues). -**Is the API documentation up to date? Are all client calls written against the docs?** +Examples: -We use [swagger](https://github.com/coralproject/talk/blob/master/swagger.yaml) to track our API documentation. - -* If APIs are created or updated, is the swagger.yml file up to date? There's nothing more frustrating than trying to develop against docs that are out of date or wrong. We need to be meticulous here as it's the little differences that can cause the most frustration and tricky bugs. -* If client code calls APIs, are they written against the swagger.yml file? Are all return codes handled? - -**Is there sufficient test coverage?** - -Our tests folder is set up to mirror the code folders: [https://github.com/coralproject/talk/tree/master/tests](https://github.com/coralproject/talk/tree/master/tests) - -* Can you a sense of the logic behind the code by reading the tests? -* Can you see both what should happen and what should _never, ever_ be allowed to happen? -* Are there future cases that are guarded against via the creation of unit tests (aka, making sure things are typed, specifically checking for all values that will be used, etc...)? +* [Add An Emoji Button to Comments](https://github.com/coralproject/talk/wiki/Add-An-Emoji-Button-to-Comments) -### Forking, Branching and Merging +### Work on the Core -Talk follows the _master as tip_ repo structure. `master` is the bleeding edge. It should be _as stable as possible_ but may suffer instabilities, generally during times that fundamental architectural elements are added. +There is always more work to be done to make an application more stable, scaleable and secure. -Releases are _tagged_ off the master branch. +If you see issues in the code or have ideas on how we may improve Talk, please consider: -Contributions to Talk follow this process. There are a lot of steps, but mechanically following these steps will standardize communication, help stop errors and let you focus on your contribution. - -* At the outset of a piece of work, a branch or fork is made from master. -* The work is done in that fork. -* As soon as the work has taken shape, a PR is created for discussion. (If the PR is created for review before it's ready to merge, please make that clear in the description/title.) -* At least one other contributor to the project must review all code (see Code Reviews below.) -* If there are merge conflicts with master, merge master into the branch. -* Ensure that [circleci](https://circleci.com/) passes all tests for your branch. (If you have forked and do not have circleci set up, you and the reviewer should independently ensure that all the of Continuous Integration steps pass before merging.) -* If merge conflicts exist with `master`, merge `master` into your branch and re-run CI before merging into master. -* Merge to master, but _you're not quite done yet!_ -* Deploy master to staging (or have a core member do so.) -* Ensure that all your changes are working on staging. -* Have your reviewer verify the same. -* ... aaaand the work is delivered! +* [contributing a fix](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging), +* [filing an issue](https://github.com/coralproject/talk/issues), or +* or otherwise [letting us know](https://github.com/coralproject/talk/wiki/Contact-Us). -## Continuous Integration -We use circleci to run our ci: [https://circleci.com/gh/coralproject/talk](https://circleci.com/gh/coralproject/talk) - -Our pipeline will _test_, _lint_, and _build_ all pushes to the repo. - -Any branch not passing CI will not be merged into master. - -If you're working in a fork, please run each of the steps locally before submitting a PR. - - -## Coding Style - -### API Design - -When building APIs, we follow these principles: - -* Follow [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) principles for basic operations. -* Avoid routing yourself into a corner, for example, by putting a variable other than an object's id directly after an object. -* Put non-required, flexible variables into query params, required/identity based values in request params. diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 000000000..faee7bb25 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,95 @@ +# Installing a dev environment + +By contributing to this project you agree to the [Code of Conduct](https://coralproject.net/code-of-conduct.html). + +## Requirements + +### System + +- Any flavor of Linux, OSX or Windows +- 1GB memory (minimum) +- 5GB storage (minimum) + +### Software + +* [Node](https://nodejs.org/es/download/package-manager) v7 or later +* Mongo v3.2 or later +* Redis v3.2 or later + +_Please be sure to check the versions of these requirements. Insufficient versions of these may lead to unexpected errors!_ + +## First time setup + +### Installation + +Navigate to a directory. + +``` +git clone https://github.com/coralproject/talk +cd talk +npm install +``` + +### Environmental Variables + +Talk uses environmental variables for configuration. You can learn about them in the [README file](README.md). + + +## Workflows + +### The server + +Starting the server: + +``` +npm start +``` + +Browse to `http://localhost:3000` (or your custom port.) + +### Building the front end + +Our build process will build all front end components registered [here](https://github.com/coralproject/talk/blob/6052cac1d3494f8060325a88bb2ce03c88c2f94c/webpack.config.dev.js#L9-L15). + +One time build: + +``` +npm build +``` + +Build, then rebuild when a file is updated (development build): + +``` +npm build-watch +``` + + +### Testing + +Run all tests once: + +` +npm test +` + +Run our end to end tests (will install Selenium and nightwatch): + +` +npm run e2e +` + +_Please ensure all tests are passing before submitting a PR!_ + +## Troubleshooting + + +##### Can't ping the redis server! + +- Check that Redis Server is running. +- Check that TALK_REDIS_URL is set. + +##### Authenticaiton doesn't work! + +- Make sure Redis is the correct version. + + diff --git a/README.md b/README.md index 3b258736f..239483628 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,17 @@ A commenting platform from [The Coral Project](https://coralproject.net). ## Contributing to Talk -### Product Roadmap - -You can view what the Coral Team is working on next here https://www.pivotaltracker.com/n/projects/1863625. - -You can view product ideas and our longer term roadmap here https://trello.com/b/ILND751a/talk. +See our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md)! ## Usage +### Installation + +To set up a development environment or build from source, see [INSTALL.md](https://github.com/coralproject/talk/blob/master/INSTALL.md). + +To launch a Talk server of your own from your browser without any need to muck about in a terminal or think about engineering concepts, stay tuned. We will launch [our installer](https://github.com/coralproject/talk-install) shortly!! + + ### Configuration The Talk application requires specific configuration options to be available @@ -34,6 +37,11 @@ available in the format: `://` without the path. - `TALK_SMTP_HOST` (*required*) - SMTP host url with format `smtp.domain.com`. - `TALK_SMTP_PORT` (*required*) - SMTP port. + +### Install from Source + +If you want to run Talk in development mode from source (without docker) you can read the [INSTALL file](INSTALL.md). + ### License Copyright 2016 Mozilla Foundation diff --git a/app.js b/app.js index 79d09db65..b128eb283 100644 --- a/app.js +++ b/app.js @@ -5,8 +5,10 @@ const path = require('path'); const helmet = require('helmet'); const passport = require('./services/passport'); const session = require('express-session'); +const enabled = require('debug').enabled; const RedisStore = require('connect-redis')(session); const redis = require('./services/redis'); +const csrf = require('csurf'); const app = express(); @@ -42,6 +44,7 @@ const session_opts = { rolling: true, saveUninitialized: false, resave: false, + unset: 'destroy', name: 'talk.sid', cookie: { secure: false, @@ -73,6 +76,29 @@ app.use(session(session_opts)); app.use(passport.initialize()); app.use(passport.session()); +//============================================================================== +// CSRF MIDDLEWARE +//============================================================================== + +if (process.env.TEST_MODE === 'unit') { + + // Add this fake test token in the event we are in unit test mode, and don't + // include the CSRF protection. + app.locals.csrfToken = 'UNIT_TESTS'; + +} else { + + // Setup route middlewares for CSRF protection. + // Default ignore methods are GET, HEAD, OPTIONS + app.use(csrf({})); + app.use((req, res, next) => { + res.locals.csrfToken = req.csrfToken(); + + next(); + }); + +} + //============================================================================== // ROUTES //============================================================================== @@ -95,7 +121,7 @@ app.use((req, res, next) => { // returning a status code that makes sense. app.use('/api', (err, req, res, next) => { if (err !== ErrNotFound) { - if (app.get('env') !== 'test') { + if (app.get('env') !== 'test' || enabled('talk:errors')) { console.error(err); } } diff --git a/bin/cli-jobs b/bin/cli-jobs index 60a7b8efb..879927197 100755 --- a/bin/cli-jobs +++ b/bin/cli-jobs @@ -6,6 +6,7 @@ const program = require('commander'); const scraper = require('../services/scraper'); +const mailer = require('../services/mailer'); const util = require('../util'); const mongoose = require('../services/mongoose'); const kue = require('../services/kue'); @@ -19,13 +20,16 @@ util.onshutdown([ */ function processJobs() { - // Start the processor. + // Start the scraper processor. scraper.process(); + // Start the mail processor. + mailer.process(); + // The scraper only needs to shutdown when the scraper has actually been // started. util.onshutdown([ - () => scraper.shutdown() + () => kue.Task.shutdown() ]); } diff --git a/bin/cli-serve b/bin/cli-serve index 05fe6efc7..c8cf37a47 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -5,6 +5,8 @@ const debug = require('debug')('talk:server'); const http = require('http'); const init = require('../init'); const scraper = require('../services/scraper'); +const mailer = require('../services/mailer'); +const kue = require('../services/kue'); const mongoose = require('../services/mongoose'); const util = require('../util'); @@ -12,7 +14,7 @@ const util = require('../util'); * Get port from environment and store in Express. */ -const port = normalizePort(process.env.TALK_PORT || (process.env.NODE_ENV === 'test' ? '3011' : '3000')); +const port = normalizePort(process.env.TALK_PORT || '3000'); app.set('port', port); @@ -119,15 +121,18 @@ startApp(); // Enable job processing on the thread if enabled. if (program.jobs) { - // Start the processor. + // Start the scraper processor. scraper.process(); + + // Start the mail processor. + mailer.process(); } // Define a safe shutdown function to call in the event we need to shutdown // because the node hooks are below which will interrupt the shutdown process. // Shutdown the mongoose connection, the app server, and the scraper. util.onshutdown([ - () => program.jobs ? scraper.shutdown() : null, + () => program.jobs ? kue.Task.shutdown() : null, () => mongoose.disconnect(), () => server.close() ]); diff --git a/bin/cli-users b/bin/cli-users index ae688e122..fd9e30c7f 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -80,12 +80,16 @@ function createUser(options) { .then((user) => { console.log(`Created user ${user.id}.`); - return User - .addRoleToUser(user.id, result.role.trim()) - .then(() => { - console.log(`Added the admin ${result.role.trim()} to User ${user.id}.`); - util.shutdown(); - }); + if (result.role && result.role.length > 0) { + return User + .addRoleToUser(user.id, result.role.trim()) + .then(() => { + console.log(`Added the admin ${result.role.trim()} to User ${user.id}.`); + util.shutdown(); + }); + } else { + util.shutdown(); + } }) .catch((err) => { console.error(err); diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index bc9747f34..9c0f271cc 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -1,7 +1,7 @@ import React from 'react'; import {Router, Route, IndexRoute, browserHistory} from 'react-router'; -import ModerationQueue from 'containers/ModerationQueue/ModerationQueue'; +import ModerationContainer from 'containers/ModerationQueue/ModerationContainer'; import CommentStream from 'containers/CommentStream/CommentStream'; import Configure from 'containers/Configure/Configure'; import Streams from 'containers/Streams/Streams'; @@ -10,7 +10,7 @@ import LayoutContainer from 'containers/LayoutContainer'; const routes = ( - + diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 54763259d..fb469588b 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -10,9 +10,9 @@ const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); export const checkLogin = () => dispatch => { dispatch(checkLoginRequest()); coralApi('/auth') - .then(user => { - const isAdmin = !!user.roles.filter(i => i === 'admin').length; - dispatch(checkLoginSuccess(user, isAdmin)); + .then(result => { + const isAdmin = !!result.user.roles.filter(i => i === 'admin').length; + dispatch(checkLoginSuccess(result.user, isAdmin)); }) .catch(error => dispatch(checkLoginFailure(error))); }; diff --git a/client/coral-admin/src/actions/comments.js b/client/coral-admin/src/actions/comments.js index 5d2104560..e8276a9da 100644 --- a/client/coral-admin/src/actions/comments.js +++ b/client/coral-admin/src/actions/comments.js @@ -34,9 +34,9 @@ export const fetchModerationQueueComments = () => { // Create a new comment export const createComment = (name, body) => { - return dispatch => { - const comment = {body, name}; - return coralApi('/comments', {method: 'POST', comment}) + return (dispatch) => { + const formData = {body, name}; + return coralApi('/comments', {method: 'POST', body: formData}) .then(res => dispatch({type: commentTypes.COMMENT_CREATE_SUCCESS, comment: res})) .catch(error => dispatch({type: commentTypes.COMMENT_CREATE_FAILED, error})); }; diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index c4712835a..c2d738460 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -41,14 +41,15 @@ export const newPage = () => ({ type: COMMENTERS_NEW_PAGE }); -export const setRole = (id, role) => dispatch => { +export const setRole = (id, role) => (dispatch) => { + return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}}) .then(() => { return dispatch({type: SET_ROLE, id, role}); }); }; -export const setCommenterStatus = (id, status) => dispatch => { +export const setCommenterStatus = (id, status) => (dispatch) => { return coralApi(`/users/${id}/status`, {method: 'POST', body: {status}}) .then(() => { return dispatch({type: SET_COMMENTER_STATUS, id, status}); diff --git a/client/coral-admin/src/actions/users.js b/client/coral-admin/src/actions/users.js index bc42a7a4c..30d9cd34b 100644 --- a/client/coral-admin/src/actions/users.js +++ b/client/coral-admin/src/actions/users.js @@ -6,7 +6,7 @@ import * as actions from '../constants/user'; */ // change status of a user export const userStatusUpdate = (status, userId, commentId) => { - return dispatch => { + return (dispatch) => { dispatch({type: actions.UPDATE_STATUS_REQUEST}); return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}}) .then(res => dispatch({type: actions.UPDATE_STATUS_SUCCESS, res})) diff --git a/client/coral-admin/src/components/BanUserDialog.js b/client/coral-admin/src/components/BanUserDialog.js index 1b4af6eb1..b151d3020 100644 --- a/client/coral-admin/src/components/BanUserDialog.js +++ b/client/coral-admin/src/components/BanUserDialog.js @@ -1,45 +1,46 @@ import React from 'react'; - import {Dialog} from 'coral-ui'; -import Button from 'coral-ui/components/Button'; - import styles from './BanUserDialog.css'; +import Button from 'coral-ui/components/Button'; + import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; const lang = new I18n(translations); -const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => { - const {userName = '', userId = '', commentId = ''} = user; - - return ( - handleClose()} onCancel={() => handleClose()} title={lang.t('bandialog.ban_user')}> - handleClose()}>× -
-
-

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

-
-
-

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

- - {lang.t('bandialog.note')} - -
-
- - -
+const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => ( + handleClose()} + onCancel={() => handleClose()} + title={lang.t('bandialog.ban_user')}> + × +
+
+

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

+
+

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

+ + {lang.t('bandialog.note')} + +
+
+ + +
+
- ); -}; +); export default BanUserDialog; diff --git a/client/coral-admin/src/components/CommentList.js b/client/coral-admin/src/components/CommentList.js index 326d63cf1..b5eb40231 100644 --- a/client/coral-admin/src/components/CommentList.js +++ b/client/coral-admin/src/components/CommentList.js @@ -21,14 +21,12 @@ export default class CommentList extends React.Component { comments: PropTypes.object.isRequired, users: PropTypes.object.isRequired, onClickAction: PropTypes.func, - modActions: PropTypes.arrayOf(PropTypes.string), + + // list of actions (flags, etc) associated with the comments + modActions: PropTypes.arrayOf(PropTypes.string).isRequired, loading: PropTypes.bool, - // list of actions (flags, etc) associated with the comments - actions: PropTypes.shape({ - ids: PropTypes.arrayOf(PropTypes.string) - }), - suspectWords: PropTypes.arrayOf(PropTypes.string) + suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired } constructor (props) { diff --git a/client/coral-admin/src/constants/auth.js b/client/coral-admin/src/constants/auth.js index c6cb6c944..f77deb670 100644 --- a/client/coral-admin/src/constants/auth.js +++ b/client/coral-admin/src/constants/auth.js @@ -2,6 +2,8 @@ export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST'; export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS'; export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE'; +export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN'; + export const LOGOUT_REQUEST = 'LOGOUT_REQUEST'; export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; export const LOGOUT_FAILURE = 'LOGOUT_FAILURE'; diff --git a/client/coral-admin/src/containers/CommentStream/CommentStream.js b/client/coral-admin/src/containers/CommentStream/CommentStream.js index 556e50463..ca5f6b398 100644 --- a/client/coral-admin/src/containers/CommentStream/CommentStream.js +++ b/client/coral-admin/src/containers/CommentStream/CommentStream.js @@ -43,7 +43,7 @@ class CommentStream extends React.Component { render ({comments, users}, {snackbar, snackbarMsg}) { return (
- + this.setState({singleView: !this.state.singleView})); + key('shift+/', () => this.setState({modalOpen: true})); + key('esc', () => this.setState({modalOpen: false})); + } + + componentWillUnmount() { + key.unbind('s'); + key.unbind('shift+/'); + key.unbind('esc'); + } + + componentDidMount() { + + // Hack for dynamic mdl tabs + if (typeof componentHandler !== 'undefined') { + + // FIXME: fix this hack + componentHandler.upgradeAllRegistered(); // eslint-disable-line no-undef + } + } + + onTabClick(activeTab) { + this.setState({activeTab}); + } + + onClose() { + this.setState({modalOpen: false}); + } + + render () { + const {comments} = 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); + + return ( + + ); + } +} + +const mapStateToProps = state => ({ + comments: state.comments.toJS(), + settings: state.settings.toJS(), + users: state.users.toJS() +}); + +const mapDispatchToProps = dispatch => { + return { + fetchSettings: () => dispatch(fetchSettings()), + fetchModerationQueueComments: () => dispatch(fetchModerationQueueComments()), + showBanUserDialog: (userId, userName, commentId) => dispatch(showBanUserDialog(userId, userName, commentId)), + hideBanUserDialog: () => dispatch(hideBanUserDialog(false)), + banUser: (userId, commentId) => dispatch(userStatusUpdate('banned', userId, commentId)).then(() => { + dispatch(fetchModerationQueueComments()); + }), + updateStatus: (action, comment) => dispatch(updateStatus(action, comment)) + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(ModerationContainer); diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index 0a6c129fd..55a2e705e 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -1,168 +1,71 @@ import React from 'react'; -import {connect} from 'react-redux'; -import key from 'keymaster'; +import styles from './ModerationQueue.css'; import ModerationKeysModal from 'components/ModerationKeysModal'; import CommentList from 'components/CommentList'; import BanUserDialog from 'components/BanUserDialog'; -import { - updateStatus, - showBanUserDialog, - hideBanUserDialog, - fetchModerationQueueComments -} from 'actions/comments'; -import {userStatusUpdate} from 'actions/users'; -import {fetchSettings} from 'actions/settings'; -import styles from './ModerationQueue.css'; - import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../../translations.json'; -/* - * Renders the moderation queue as a tabbed layout with 3 moderation - * queues : - * * pending: filtered by status Untouched - * * rejected: filtered by status Rejected - * * flagged: with a flagged action on them - */ - -class ModerationQueue extends React.Component { - - constructor (props) { - super(props); - - this.state = {activeTab: 'pending', singleView: false, modalOpen: false}; - } - - // Fetch comments and bind singleView key before render - componentWillMount () { - this.props.dispatch(fetchSettings()); - this.props.dispatch(fetchModerationQueueComments()); - key('s', () => this.setState({singleView: !this.state.singleView})); - key('shift+/', () => this.setState({modalOpen: true})); - key('esc', () => this.setState({modalOpen: false})); - } - - // Unbind singleView key before unmount - componentWillUnmount () { - key.unbind('s'); - key.unbind('shift+/'); - key.unbind('esc'); - } - - // Hack for dynamic mdl tabs - componentDidMount () { - if (typeof componentHandler !== 'undefined') { - - // FIXME: fix this hack - componentHandler.upgradeAllRegistered(); // eslint-disable-line no-undef - } - } - - // Dispatch the update status action - onCommentAction (action, comment) { - - // If not banning then change the status to approved or flagged as action = status - this.props.dispatch(updateStatus(action, comment)); - } - - showBanUserDialog (userId, userName, commentId) { - this.props.dispatch(showBanUserDialog(userId, userName, commentId)); - } - - hideBanUserDialog () { - this.props.dispatch(hideBanUserDialog(false)); - } - - banUser (userId, commentId) { - this.props.dispatch(userStatusUpdate('banned', userId, commentId)) - .then(() => { - this.props.dispatch(fetchModerationQueueComments()); - }); - } - - onTabClick (activeTab) { - this.setState({activeTab}); - } - - // Render the tabbed lists moderation queues - render () { - const {comments, users, settings} = this.props; - const {activeTab, singleView, modalOpen} = this.state; - - 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); - - return ( -
-
- -
- this.onCommentAction(action, comment)} - onClickShowBanDialog={(userId, userName, commentId) => this.showBanUserDialog(userId, userName, commentId)} - modActions={['reject', 'approve', 'ban']} - loading={comments.loading} /> - this.hideBanUserDialog()} - onClickBanUser={(userId, commentId) => this.banUser(userId, commentId)} - user={comments.banUser}/> -
-
- this.onCommentAction(action, comment)} - modActions={['approve']} - loading={comments.loading} /> -
-
- this.onCommentAction(action, comment)} - modActions={['reject', 'approve']} - loading={comments.loading} /> -
- this.setState({modalOpen: false})} /> -
-
- ); - } -} - -const mapStateToProps = state => ({ - actions: state.actions.toJS(), - settings: state.settings.toJS(), - comments: state.comments.toJS(), - users: state.users.toJS() -}); - -export default connect(mapStateToProps)(ModerationQueue); - const lang = new I18n(translations); + +export default ({onTabClick, ...props}) => ( + +); diff --git a/client/coral-admin/src/reducers/settings.js b/client/coral-admin/src/reducers/settings.js index b05418715..12b16d9ad 100644 --- a/client/coral-admin/src/reducers/settings.js +++ b/client/coral-admin/src/reducers/settings.js @@ -37,7 +37,7 @@ const updateSettings = (state, action) => { // any nested settings must have a specialized setter const updateWordlist = (state, action) => { - return state.setIn(['settings', 'wordlist', action.listName], action.wordlist); + return state.setIn(['settings', 'wordlist', action.listName], action.list); }; const saveComplete = (state, action) => { diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 3397a521b..0fbd1e7b9 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -48,7 +48,7 @@ "include-text": "Include your text here.", "comment-settings": "Comment Settings", "embed-comment-stream": "Embed Comment Stream", - "banned-word-header": "Write the bannned words list", + "banned-word-header": "Write the banned words list", "suspect-word-header": "Write the suspect words list", "banned-word-text": "Comments which contain these words or phrases (not case-sensitive) will be automatically removed from the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.", "suspect-word-text": "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.", @@ -146,6 +146,7 @@ "moderate": "Moderar", "configure": "Configurar", "community": "Comunidad", + "streams": "Streams", "closed-comments-desc": "Escribe un mensaje para cuando los comentarios se encuentran cerrados", "closed-comments-label": "Escribe un mensaje...", "never": "Nunca", diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index 7d1e6c612..bd60f8c32 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -138,7 +138,7 @@ class CommentStream extends Component {
:

{closedMessage}

} - {!loggedIn && } + {!loggedIn && } { rootItem.comments && rootItem.comments.map((commentId) => { const comment = comments[commentId]; diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index c5390607c..0f65e14c2 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -23,7 +23,7 @@ const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST}); const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin}); const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error}); -export const fetchSignIn = (formData) => dispatch => { +export const fetchSignIn = (formData) => (dispatch) => { dispatch(signInRequest()); coralApi('/auth/local', {method: 'POST', body: formData}) .then(({user}) => { @@ -72,8 +72,9 @@ const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST}); const signUpSuccess = user => ({type: actions.FETCH_SIGNUP_SUCCESS, user}); const signUpFailure = error => ({type: actions.FETCH_SIGNUP_FAILURE, error}); -export const fetchSignUp = formData => dispatch => { +export const fetchSignUp = formData => (dispatch) => { dispatch(signUpRequest()); + coralApi('/users', {method: 'POST', body: formData}) .then(({user}) => { dispatch(signUpSuccess(user)); @@ -81,7 +82,9 @@ export const fetchSignUp = formData => dispatch => { dispatch(changeView('SIGNIN')); }, 3000); }) - .catch(() => dispatch(signUpFailure(lang.t('error.emailInUse')))); // We need to inprove error handling. TODO (bc) + .catch(error => { + dispatch(signUpFailure(lang.t(`error.${error.message}`))); + }); }; // Forgot Password Actions @@ -90,9 +93,9 @@ const forgotPassowordRequest = () => ({type: actions.FETCH_FORGOT_PASSWORD_REQUE const forgotPassowordSuccess = () => ({type: actions.FETCH_FORGOT_PASSWORD_SUCCESS}); const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILURE}); -export const fetchForgotPassword = email => dispatch => { +export const fetchForgotPassword = email => (dispatch) => { dispatch(forgotPassowordRequest(email)); - coralApi('/users/request-password-reset', {method: 'POST', body: {email}}) + coralApi('/account/password/reset', {method: 'POST', body: {email}}) .then(() => dispatch(forgotPassowordSuccess())) .catch(error => dispatch(forgotPassowordFailure(error))); }; @@ -124,13 +127,13 @@ const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); export const checkLogin = () => dispatch => { dispatch(checkLoginRequest()); coralApi('/auth') - .then(user => { - if (!user) { + .then((result) => { + if (!result.user) { throw new Error('Not logged in'); } - const isAdmin = !!user.roles.filter(i => i === 'admin').length; - dispatch(checkLoginSuccess(user, isAdmin)); + const isAdmin = !!result.user.roles.filter(i => i === 'admin').length; + dispatch(checkLoginSuccess(result.user, isAdmin)); }) .catch(error => dispatch(checkLoginFailure(error))); }; diff --git a/client/coral-framework/actions/items.js b/client/coral-framework/actions/items.js index 243429863..48418b2a2 100644 --- a/client/coral-framework/actions/items.js +++ b/client/coral-framework/actions/items.js @@ -220,8 +220,12 @@ export function postItem (item, type, id) { */ export function postAction (item_id, item_type, action) { - return () => { - return coralApi(`/${item_type}/${item_id}/actions`, {method: 'POST', body: action}); + return (dispatch) => { + return coralApi(`/${item_type}/${item_id}/actions`, {method: 'POST', body: action}) + .then((json) => { + dispatch(updateItem(action.item_id, action.action_type, action.id, item_type)); + return json; + }); }; } diff --git a/client/coral-framework/actions/user.js b/client/coral-framework/actions/user.js index 0ee8659d8..aeb8d0989 100644 --- a/client/coral-framework/actions/user.js +++ b/client/coral-framework/actions/user.js @@ -14,10 +14,10 @@ const saveBioFailure = error => ({type: actions.SAVE_BIO_FAILURE, error}); export const saveBio = (user_id, formData) => dispatch => { dispatch(saveBioRequest()); - coralApi(`/users/${user_id}/bio`, {method: 'PUT', body: formData}) - .then(({settings}) => { + coralApi('/account/settings', {method: 'PUT', body: formData}) + .then(() => { dispatch(addNotification('success', lang.t('successBioUpdate'))); - dispatch(saveBioSuccess(settings)); + dispatch(saveBioSuccess(formData)); }) .catch(error => dispatch(saveBioFailure(error))); }; @@ -42,8 +42,6 @@ export const fetchCommentsByUserId = userId => { dispatch({type: assetActions.MULTIPLE_ASSETS_SUCCESS, assets: assets.map(asset => asset.id)}); }) .catch(error => { - console.error(error.stack); - console.error('FAILURE_COMMENTS_BY_USER', error); dispatch({type: actions.COMMENTS_BY_USER_FAILURE, error}); }); }; diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index 07ca2e661..5742adf75 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -31,3 +31,4 @@ export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST'; export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS'; export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE'; +export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN'; diff --git a/client/coral-framework/constants/user.js b/client/coral-framework/constants/user.js index 6e09726d3..9c6f508fe 100644 --- a/client/coral-framework/constants/user.js +++ b/client/coral-framework/constants/user.js @@ -4,3 +4,4 @@ export const SAVE_BIO_FAILURE = 'SAVE_BIO_FAILURE'; export const COMMENTS_BY_USER_REQUEST = 'COMMENTS_BY_USER_REQUEST'; export const COMMENTS_BY_USER_SUCCESS = 'COMMENTS_BY_USER_SUCCESS'; export const COMMENTS_BY_USER_FAILURE = 'COMMENTS_BY_USER_FAILURE'; +export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; diff --git a/client/coral-framework/helpers/response.js b/client/coral-framework/helpers/response.js index 1b4340dc4..e4e6e7c37 100644 --- a/client/coral-framework/helpers/response.js +++ b/client/coral-framework/helpers/response.js @@ -2,16 +2,30 @@ export const base = '/api/v1'; const buildOptions = (inputOptions = {}) => { + const csurfDOM = document.head.querySelector('[property=csrf]'); + const defaultOptions = { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, - credentials: 'same-origin' + credentials: 'same-origin', + _csrf: csurfDOM ? csurfDOM.content : false }; + const options = Object.assign({}, defaultOptions, inputOptions); + if (options._csrf) { + switch (options.method.toLowerCase()) { + case 'post': + case 'put': + case 'delete': + options.headers['x-csrf-token'] = options._csrf; + break; + } + } + if (options.method.toLowerCase() !== 'get') { options.body = JSON.stringify(options.body); } @@ -23,7 +37,13 @@ const handleResp = res => { if (res.status === 401) { throw new Error('Not Authorized to make this request'); } else if (res.status > 399) { - throw new Error('Error! Status ', res.status); + return res.json().then(err => { + let message = err.message || res.status; + if (err.error && err.error.translation_key) { + message = err.error.translation_key; + } + throw new Error(message); + }); } else if (res.status === 204) { return res.text(); } else { diff --git a/client/coral-framework/helpers/validate.js b/client/coral-framework/helpers/validate.js index 8c5ebd36f..80efb1b1f 100644 --- a/client/coral-framework/helpers/validate.js +++ b/client/coral-framework/helpers/validate.js @@ -2,5 +2,5 @@ export default { email: email => (/^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)), password: pass => (/^(?=.{8,}).*$/.test(pass)), confirmPassword: () => true, - displayName: displayName => (/^(?=.{3,}).*$/.test(displayName)) + displayName: displayName => (/^[a-z0-9_]+$/.test(displayName)) }; diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index d32956b84..36f8e0764 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -41,6 +41,9 @@ export default function auth (state = initialState, action) { .set('view', action.view); case actions.CLEAN_STATE: return initialState; + case actions.CHECK_CSRF_TOKEN: + return state + .set('_csrf', action._csrf); case actions.FETCH_SIGNIN_REQUEST: return state .set('isLoading', true); diff --git a/client/coral-framework/reducers/user.js b/client/coral-framework/reducers/user.js index bd5f78e87..7e0604c4b 100644 --- a/client/coral-framework/reducers/user.js +++ b/client/coral-framework/reducers/user.js @@ -31,12 +31,13 @@ export default function user (state = initialState, action) { case authActions.FETCH_SIGNIN_FACEBOOK_FAILURE: return initialState; case actions.SAVE_BIO_SUCCESS: - return state - .set('settings', action.settings); + return state.set('settings', action.settings); case actions.COMMENTS_BY_USER_SUCCESS: return state.set('myComments', action.comments); case assetActions.MULTIPLE_ASSETS_SUCCESS: return state.set('myAssets', action.assets); + case actions.LOGOUT_SUCCESS: + return initialState; default : return state; } diff --git a/client/coral-framework/translations.json b/client/coral-framework/translations.json index 06a39944a..112f7db1a 100644 --- a/client/coral-framework/translations.json +++ b/client/coral-framework/translations.json @@ -7,10 +7,16 @@ "error": { "email": "Not a valid E-Mail", "password": "Password must be at least 8 characters", - "displayName": "Display name is too short", - "confirmPassword": "Passwords don`t match. Please, check again", + "displayName": "Display names can contain letters, numbers and _ only", + "confirmPassword": "Passwords don't match. Please, check again", "emailPasswordError": "Email and/or password combination incorrect.", - "emailInUse": "Email address already in use" + "EMAIL_REQUIRED": "An email address is required", + "PASSWORD_REQUIRED": "Must input a password", + "PASSWORD_LENGTH": "Password is too short", + "EMAIL_IN_USE": "Email address already in use", + "DISPLAY_NAME_REQUIRED": "Must input a display name", + "NO_SPECIAL_CHARACTERS": "Display names can contain letters, numbers and _ only", + "PROFANITY_ERROR": "Display names must not contain profanity. Please contact the administrator if you believe this to be in error." } }, "es": { @@ -21,10 +27,16 @@ "error": { "email": "No es un email válido", "password": "La contraseña debe tener por lo menos 8 caracteres", - "displayName": "El nombre es muy corto", + "displayName": "Los nombres pueden contener letras, números y _", "confirmPassword": "Las contraseñas no coinciden", "emailPasswordError": "Email y/o contraseña incorrecta.", - "emailInUse": "Email address already in use" + "EMAIL_REQUIRED": "Se requiere una dirección de correo electrónico", + "PASSWORD_REQUIRED": "Debe ingresar una contraseña", + "PASSWORD_LENGTH": "La contraseña es muy corta", + "EMAIL_IN_USE": "La dirección de correo electrónico se encuentra en uso", + "DISPLAY_NAME_REQUIRED": "Debe ingresar un nombre", + "NO_SPECIAL_CHARACTERS": "Los nombres pueden contener letras, números y _", + "PROFANITY_ERROR": "Los nombres no pueden contener blasfemias. Por favor contacte al administrador si cree que esto es un error" } } } diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/FlagButton.js index 097ac9d5b..23943318d 100644 --- a/client/coral-plugin-flags/FlagButton.js +++ b/client/coral-plugin-flags/FlagButton.js @@ -102,7 +102,7 @@ class FlagButton extends Component { const popupMenu = getPopupMenu[this.state.step](this.state.itemType); return
-