diff --git a/.gitignore b/.gitignore index 0ab845e4e..d30e961bd 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ client/coral-framework/graphql/introspection.json *.DS_STORE coverage/ +test/e2e/reports/ +test/e2e/bslocal.log +test/e2e/selenium-debug.log +browserstack.err plugins.json plugins/* diff --git a/.nsprc b/.nsprc new file mode 100644 index 000000000..071431eff --- /dev/null +++ b/.nsprc @@ -0,0 +1,5 @@ +{ + "exceptions": [ + "https://nodesecurity.io/advisories/531" + ] +} \ No newline at end of file diff --git a/Dockerfile.onbuild b/Dockerfile.onbuild index b9cf8ce87..20d34ffb9 100644 --- a/Dockerfile.onbuild +++ b/Dockerfile.onbuild @@ -1,11 +1,18 @@ FROM coralproject/talk:latest +# Setup the build arguments +ONBUILD ARG TALK_THREADING_LEVEL=3 +ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all +ONBUILD ARG TALK_DEFAULT_LANG=en +ONBUILD ARG TALK_PLUGINS_JSON + # Bundle app source ONBUILD COPY . /usr/src/app -# At this stage, we need to install the development dependancies again because -# we need to have webpack available. We then build the new dependancies and -# clear out the development dependancies again. After this we of course need to +# At this stage, we need to install the development dependencies again because +# we need to have webpack available. We then build the new dependencies and +# clear out the development dependencies again. After this we of course need to # clear out the yarn cache, this saves quite a lot of size. ONBUILD RUN cli plugins reconcile && \ - yarn build + yarn build && \ + yarn cache clean \ No newline at end of file diff --git a/LICENSE b/LICENSE index 597d8fa73..e0a687532 100644 --- a/LICENSE +++ b/LICENSE @@ -4,8 +4,12 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. -See the License for the specific language governing permissions and limitations under the License. +See the License for the specific language governing permissions +and limitations under the License. diff --git a/README.md b/README.md index 057b29cf1..89474bf6d 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,37 @@ -# Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) +# Talk · [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) · [![NSP Status](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8/badge)](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8) · [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md#pull-requests) -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore) - -Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html). +Online comments are broken. Our open-source commenting platform, Talk, rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html). Built with <3 by The Coral Project & Mozilla. -## Getting Started +## Try Talk! -Check out our Docs: https://coralproject.github.io/talk/ +You're just one click away from trying Talk - all you need is a Heroku account and a few minutes of your time. + +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore) + +## Technical Documentation + +From getting up and running, to advanced configuration, to how to scale Talk, our [Talk Technical Docs](https://coralproject.github.io/talk/) have everything you need to know. + +## Product Guide + +Learn more about Talk, including a deep dive into features for commenters and moderators, and FAQs in our [Talk Product Guide](https://coralproject.github.io/talk/how-talk-works). ## Relevant Links -- Blog: https://blog.coralproject.net/ -- Community Forums: https://community.coralproject.net/ -- Community Guides for Journalism: https://guides.coralproject.net/ -- Project: https://coralproject.net/ -- Roadmap: https://www.pivotaltracker.com/n/projects/1863625 +- [Our Blog](https://blog.coralproject.net/) +- [Community Forums](https://community.coralproject.net/) +- [Community Guides for Journalism](https://guides.coralproject.net/) +- [More About Us](https://coralproject.net/) +- [Talk Roadmap](https://www.pivotaltracker.com/n/projects/1863625) + +## End-to-End Testing + +Talk uses [Nightwatch](https://nightwatchjs.org/) as our e2e testing framework. The testing infrastructure that allows us to run our tests in real browsers is provided with love by our friends at [Browserstack](https://browserstack.com). + +[![Browserstack](/public/img/browserstack_logo.png)](https://browserstack.com) ## License - Copyright 2017 Mozilla Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - - See the License for the specific language governing permissions and limitations under the License. +Talk is released under the [Apache License, v2.0](/LICENSE). diff --git a/bin/cli-serve b/bin/cli-serve index b04714690..4324cac92 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -1,161 +1,7 @@ #!/usr/bin/env node const program = require('./commander'); -const app = require('../app'); -const debug = require('debug')('talk:cli:serve'); -const errors = require('../errors'); -const {createServer} = require('http'); -const scraper = require('../services/scraper'); -const mailer = require('../services/mailer'); -const MigrationService = require('../services/migration'); -const SetupService = require('../services/setup'); -const kue = require('../services/kue'); -const mongoose = require('../services/mongoose'); -const util = require('./util'); -const cache = require('../services/cache'); -const {createSubscriptionManager} = require('../graph/subscriptions'); -const { - PORT -} = require('../config'); - -/** -* Get port from environment and store in Express. -*/ - -const port = normalizePort(PORT); -app.set('port', port); - -/** -* Create HTTP server. -*/ -const server = createServer(app); - -/** - * Event listener for HTTP server "error" event. - */ -function onError(error) { - if (error.syscall !== 'listen') { - throw error; - } - - let bind = typeof port === 'string' - ? `Pipe ${port}` - : `Port ${port}`; - - // handle specific listen errors with friendly messages - switch (error.code) { - case 'EACCES': - console.error(`${bind} requires elevated privileges`); - break; - case 'EADDRINUSE': - console.error(`${bind} is already in use`); - break; - } - - throw error; -} - -/** - * Normalize a port into a number, string, or false. - */ - -function normalizePort(val) { - let port = parseInt(val, 10); - - if (isNaN(port)) { - - // named pipe - return val; - } - - if (port >= 0) { - - // port number - return port; - } - - return false; -} - -/** - * Event listener for HTTP server "listening" event. - */ - -async function onListening() { - - // Start the cache instance. - await cache.init(); - - let addr = server.address(); - let bind = typeof addr === 'string' - ? `pipe ${addr}` - : `port ${addr.port}`; - debug(`API Server Listening on ${bind}`); -} - -/** - * Start the app. - */ -async function startApp(program) { - - try { - - // Check to see if the application is installed. If the application - // has been installed, then it will throw errors.ErrSettingsNotInit, this - // just means we don't have to check that the migrations have run. - await SetupService.isAvailable(); - - debug('setup is currently available, migrations not being checked'); - - } catch (e) { - - // Check the error. - switch (e) { - case errors.ErrInstallLock, errors.ErrSettingsInit: - - debug('setup is not currently available, migrations now being checked'); - - // The error was expected, just continue. - break; - default: - - // The error was not expected, throw the error! - throw e; - } - - // Now try and check the migration status. - try { - - // Verify that the minimum migration version is met. - await MigrationService.verify(); - - } catch (e) { - console.error(e); - process.exit(1); - } - - debug('migrations do not have to be run'); - } - - /** - * Listen on provided port, on all network interfaces. - */ - server.on('error', onError); - server.on('listening', onListening); - server.on('listening', () => { - - }); - server.listen(port, () => { - - // Mount the websocket server if requested. - if (program.websockets) { - debug(`Websocket Server Listening on ${port}`); - - // Mount the subscriptions server on the application server. - createSubscriptionManager(server); - } - }); -} +const serve = require('../serve'); //============================================================================== // Setting up the program command line arguments. @@ -166,24 +12,6 @@ program .option('-w, --websockets', 'enable the websocket (subscriptions) handler on this thread') .parse(process.argv); -// Start the application serving. -startApp(program); +// Start serving. +serve({jobs: program.jobs, websockets: program.websockets}); -// Enable job processing on the thread if enabled. -if (program.jobs) { - - // 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 ? kue.Task.shutdown() : null, - () => mongoose.disconnect(), - () => server.close() -]); diff --git a/bin/cli-setup b/bin/cli-setup index 2e5c1e5b8..c7a44c453 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -195,7 +195,7 @@ const performSetup = async () => { password: user.password } }); - + console.log('Settings created.'); console.log(`User ${newUser.id} created.`); console.log('\nTalk is now installed!'); diff --git a/bin/cli-users b/bin/cli-users index c6555ab28..89f53a67d 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -241,13 +241,21 @@ function listUsers() { }); users.forEach((user) => { + let state = user.disabled ? 'Disabled' : 'Enabled'; + const profile = user.profiles.find(({provider}) => provider === 'local'); + if (profile && profile.metadata && profile.metadata.confirmed_at) { + state += ', Verified'; + } else { + state += ', Unverified'; + } + table.push([ user.id, user.username, user.profiles.map((p) => p.provider).join(', '), user.roles.join(', '), user.status, - user.disabled ? 'Disabled' : 'Enabled' + state ]); }); @@ -396,6 +404,23 @@ function enableUser(userID) { }); } +/** + * Verifies an email address for a user. + * + * @param userID the user's id + * @param email the user's email address to be verified + */ +async function verify(userID, email) { + try { + await UsersService.confirmEmail(userID, email); + console.log(`User ${userID} had their email ${email} verified.`); + util.shutdown(); + } catch (err) { + console.error(err); + util.shutdown(1); + } +} + //============================================================================== // Setting up the program command line arguments. //============================================================================== @@ -467,6 +492,11 @@ program .description('enable a given user from logging in') .action(enableUser); +program + .command('verify ') + .description('verifies the given user\'s email address') + .action(verify); + program.parse(process.argv); // If there is no command listed, output help. diff --git a/circle.yml b/circle.yml index f6b35970c..9f35736c6 100644 --- a/circle.yml +++ b/circle.yml @@ -7,9 +7,7 @@ machine: environment: PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" NODE_ENV: "test" - -dependencies: - override: + pre: # 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* @@ -19,10 +17,20 @@ dependencies: # - sudo apt-get install -y mongodb-org # - sudo service mongod restart + # Install chromium for e2e and remove old google-chrome + - sudo rm -rf /opt/google/chrome + - sudo rm -f /usr/bin/google-chrome* + - sudo apt-get update + - sudo apt-get install chromium-browser + +dependencies: + override: + # Install node dependencies. - yarn --version - - yarn global add node-gyp --force + - yarn global add node-gyp nsp --force - yarn + post: # Build the static assets. - yarn build @@ -40,6 +48,9 @@ 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 + # Check dependancies using nsp. + - nsp check + - yarn e2e-ci deployment: release: diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 921f0a0a2..6a091d214 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -1,11 +1,12 @@ import queryString from 'query-string'; import { - FETCH_COMMENTERS_REQUEST, - FETCH_COMMENTERS_SUCCESS, - FETCH_COMMENTERS_FAILURE, + FETCH_USERS_REQUEST, + FETCH_USERS_SUCCESS, + FETCH_USERS_FAILURE, SORT_UPDATE, - COMMENTERS_NEW_PAGE, + SET_PAGE, + SET_SEARCH_VALUE, SET_ROLE, SET_COMMENTER_STATUS, SHOW_BANUSER_DIALOG, @@ -16,13 +17,13 @@ import { import t from 'coral-framework/services/i18n'; -export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => { - dispatch(requestFetchAccounts()); +export const fetchUsers = (query = {}) => (dispatch, _, {rest}) => { + dispatch(requestFetchUsers()); rest(`/users?${queryString.stringify(query)}`) .then(({result, page, count, limit, totalPages}) =>{ dispatch({ - type: FETCH_COMMENTERS_SUCCESS, - accounts: result, + type: FETCH_USERS_SUCCESS, + users: result, page, count, limit, @@ -32,12 +33,12 @@ export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => { .catch((error) => { console.error(error); const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); - dispatch({type: FETCH_COMMENTERS_FAILURE, error: errorMessage}); + dispatch({type: FETCH_USERS_FAILURE, error: errorMessage}); }); }; -const requestFetchAccounts = () => ({ - type: FETCH_COMMENTERS_REQUEST +const requestFetchUsers = () => ({ + type: FETCH_USERS_REQUEST }); export const updateSorting = (sort) => ({ @@ -45,8 +46,14 @@ export const updateSorting = (sort) => ({ sort }); -export const newPage = () => ({ - type: COMMENTERS_NEW_PAGE +export const setPage = (page) => ({ + type: SET_PAGE, + page, +}); + +export const setSearchValue = (value) => ({ + type: SET_SEARCH_VALUE, + value, }); export const setRole = (id, role) => (dispatch, _, {rest}) => { diff --git a/client/coral-admin/src/actions/configure.js b/client/coral-admin/src/actions/configure.js new file mode 100644 index 000000000..128de75bc --- /dev/null +++ b/client/coral-admin/src/actions/configure.js @@ -0,0 +1,13 @@ +import * as actions from 'constants/configure'; + +export const updatePending = ({updater, errorUpdater}) => { + return {type: actions.UPDATE_PENDING, updater, errorUpdater}; +}; + +export const clearPending = () => { + return {type: actions.CLEAR_PENDING}; +}; + +export const setActiveSection = (section) => { + return {type: actions.SET_ACTIVE_SECTION, section}; +}; diff --git a/client/coral-admin/src/actions/settings.js b/client/coral-admin/src/actions/settings.js deleted file mode 100644 index 35f2013c9..000000000 --- a/client/coral-admin/src/actions/settings.js +++ /dev/null @@ -1,58 +0,0 @@ -import t from 'coral-framework/services/i18n'; - -export const SETTINGS_LOADING = 'SETTINGS_LOADING'; -export const SETTINGS_RECEIVED = 'SETTINGS_RECEIVED'; -export const SETTINGS_FETCH_ERROR = 'SETTINGS_FETCH_ERROR'; - -export const SETTINGS_UPDATED = 'SETTINGS_UPDATED'; - -export const SAVE_SETTINGS_LOADING = 'SAVE_SETTINGS_LOADING'; -export const SAVE_SETTINGS_SUCCESS = 'SAVE_SETTINGS_SUCCESS'; -export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED'; - -export const WORDLIST_UPDATED = 'WORDLIST_UPDATED'; -export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED'; - -export const fetchSettings = () => (dispatch, _, {rest}) => { - dispatch({type: SETTINGS_LOADING}); - rest('/settings') - .then((settings) => { - dispatch({type: SETTINGS_RECEIVED, settings}); - }) - .catch((error) => { - console.error(error); - const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); - dispatch({type: SETTINGS_FETCH_ERROR, error: errorMessage}); - }); -}; - -// for updating top-level settings -export const updateSettings = (settings) => { - return {type: SETTINGS_UPDATED, settings}; -}; - -// this is a nested property, so it needs a special action. -export const updateWordlist = (listName, list) => { - return {type: WORDLIST_UPDATED, listName, list}; -}; - -export const updateDomainlist = (listName, list) => { - return {type: DOMAINLIST_UPDATED, listName, list}; -}; - -export const saveSettingsToServer = () => (dispatch, getState, {rest}) => { - let settings = getState().settings; - if (settings.charCount) { - settings.charCount = parseInt(settings.charCount); - } - dispatch({type: SAVE_SETTINGS_LOADING}); - rest('/settings', {method: 'PUT', body: settings}) - .then(() => { - dispatch({type: SAVE_SETTINGS_SUCCESS, settings}); - }) - .catch((error) => { - console.error(error); - const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); - dispatch({type: SAVE_SETTINGS_FAILED, error: errorMessage}); - }); -}; diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/stories.js similarity index 70% rename from client/coral-admin/src/actions/assets.js rename to client/coral-admin/src/actions/stories.js index 1ce6ad6ac..2524b0c3c 100644 --- a/client/coral-admin/src/actions/assets.js +++ b/client/coral-admin/src/actions/stories.js @@ -1,12 +1,17 @@ +import queryString from 'query-string'; + import { FETCH_ASSETS_REQUEST, FETCH_ASSETS_SUCCESS, FETCH_ASSETS_FAILURE, + SET_PAGE, + SET_SEARCH_VALUE, + SET_CRITERIA, UPDATE_ASSET_STATE_REQUEST, UPDATE_ASSET_STATE_SUCCESS, UPDATE_ASSET_STATE_FAILURE, UPDATE_ASSETS -} from '../constants/assets'; +} from '../constants/stories'; import t from 'coral-framework/services/i18n'; @@ -16,13 +21,16 @@ import t from 'coral-framework/services/i18n'; // Fetch a page of assets // Get comments to fill each of the three lists on the mod queue -export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch, _, {rest}) => { +export const fetchAssets = (query = {}) => (dispatch, _, {rest}) => { dispatch({type: FETCH_ASSETS_REQUEST}); - return rest(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`) - .then(({result, count}) => + return rest(`/assets?${queryString.stringify(query)}`) + .then(({result, page, count, limit, totalPages}) => dispatch({type: FETCH_ASSETS_SUCCESS, assets: result, - count + page, + count, + limit, + totalPages, })) .catch((error) => { console.error(error); @@ -47,3 +55,19 @@ export const updateAssetState = (id, closedAt) => (dispatch, _, {rest}) => { export const updateAssets = (assets) => (dispatch) => { dispatch({type: UPDATE_ASSETS, assets}); }; + +export const setPage = (page) => ({ + type: SET_PAGE, + page, +}); + +export const setSearchValue = (value) => ({ + type: SET_SEARCH_VALUE, + value, +}); + +export const setCriteria = (criteria) => ({ + type: SET_CRITERIA, + criteria, +}); + diff --git a/client/coral-admin/src/actions/userDetail.js b/client/coral-admin/src/actions/userDetail.js index f8b22008c..3a1de5745 100644 --- a/client/coral-admin/src/actions/userDetail.js +++ b/client/coral-admin/src/actions/userDetail.js @@ -20,3 +20,9 @@ export const toggleSelectCommentInUserDetail = (id, active) => { }; }; +export const toggleSelectAllCommentInUserDetail = (ids, active) => { + return { + type: active ? actions.SELECT_ALL_USER_DETAIL_COMMENT : actions.CLEAR_USER_DETAIL_SELECTIONS, + ids + }; +}; diff --git a/client/coral-admin/src/components/AdminLogin.js b/client/coral-admin/src/components/AdminLogin.js index be8dd8b09..d195ad72b 100644 --- a/client/coral-admin/src/components/AdminLogin.js +++ b/client/coral-admin/src/components/AdminLogin.js @@ -4,6 +4,7 @@ import Layout from 'coral-admin/src/components/ui/Layout'; import styles from './NotFound.css'; import {Button, TextField, Alert, Success} from 'coral-ui'; import Recaptcha from 'react-recaptcha'; +import cn from 'classnames'; class AdminLogin extends React.Component { @@ -34,19 +35,22 @@ class AdminLogin extends React.Component { render () { const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props; const signInForm = ( -
+ {errorMessage && {errorMessage}} this.setState({email: e.target.value})} /> this.setState({password: e.target.value})} type='password' />