diff --git a/app.js b/app.js index d230596c0..aca80a4b5 100644 --- a/app.js +++ b/app.js @@ -7,7 +7,7 @@ const passport = require('./services/passport'); const session = require('express-session'); const RedisStore = require('connect-redis')(session); const redis = require('./services/redis'); -const cookieParser = require('cookie-parser'); +const csrf = require('csurf'); const app = express(); @@ -43,6 +43,7 @@ const session_opts = { rolling: true, saveUninitialized: false, resave: false, + unset: 'destroy', name: 'talk.sid', cookie: { secure: false, @@ -66,12 +67,6 @@ if (app.get('env') === 'production') { app.use(session(session_opts)); -//============================================================================== -// CSRF MIDDLEWARE -//============================================================================== - -app.use(cookieParser()); - //============================================================================== // PASSPORT MIDDLEWARE //============================================================================== @@ -80,6 +75,29 @@ app.use(cookieParser()); 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 //============================================================================== 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/actions/auth.js b/client/coral-admin/src/actions/auth.js index 8cc094d5f..fb469588b 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -11,19 +11,12 @@ export const checkLogin = () => dispatch => { dispatch(checkLoginRequest()); coralApi('/auth') .then(result => { - if (result.csrfToken !== null) { - dispatch(check_csrf(result.csrfToken)); - } - const isAdmin = !!result.user.roles.filter(i => i === 'admin').length; dispatch(checkLoginSuccess(result.user, isAdmin)); }) .catch(error => dispatch(checkLoginFailure(error))); }; -// Set CSRF Token -export const check_csrf = (_csrf) => ({type: actions.CHECK_CSRF_TOKEN, _csrf}); - // LogOut Actions const logOutRequest = () => ({type: actions.LOGOUT_REQUEST}); diff --git a/client/coral-admin/src/actions/comments.js b/client/coral-admin/src/actions/comments.js index 98efae60d..e8276a9da 100644 --- a/client/coral-admin/src/actions/comments.js +++ b/client/coral-admin/src/actions/comments.js @@ -34,9 +34,8 @@ export const fetchModerationQueueComments = () => { // Create a new comment export const createComment = (name, body) => { - return (dispatch, getState) => { - const _csrf = getState().auth.get('_csrf'); - const formData = {body, name, _csrf}; + 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 35ba11fac..c2d738460 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -41,18 +41,16 @@ export const newPage = () => ({ type: COMMENTERS_NEW_PAGE }); -export const setRole = (id, role) => (dispatch, getState) => { +export const setRole = (id, role) => (dispatch) => { - const _csrf = getState().auth.get('_csrf'); - return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}, _csrf: _csrf}) + return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}}) .then(() => { return dispatch({type: SET_ROLE, id, role}); }); }; -export const setCommenterStatus = (id, status) => (dispatch, getState) => { - const _csrf = getState().auth.get('_csrf'); - return coralApi(`/users/${id}/status`, {method: 'POST', body: {status}, _csrf: _csrf}) +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 30c20290f..30d9cd34b 100644 --- a/client/coral-admin/src/actions/users.js +++ b/client/coral-admin/src/actions/users.js @@ -6,10 +6,9 @@ import * as actions from '../constants/user'; */ // change status of a user export const userStatusUpdate = (status, userId, commentId) => { - return (dispatch, getState) => { + return (dispatch) => { dispatch({type: actions.UPDATE_STATUS_REQUEST}); - const _csrf = getState().auth.get('_csrf'); - return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}, _csrf}) + return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}}) .then(res => dispatch({type: actions.UPDATE_STATUS_SUCCESS, res})) .catch(error => dispatch({type: actions.UPDATE_STATUS_FAILURE, error})); }; diff --git a/client/coral-admin/src/components/CommentBox.js b/client/coral-admin/src/components/CommentBox.js index fc9e6c9ea..ce3438960 100644 --- a/client/coral-admin/src/components/CommentBox.js +++ b/client/coral-admin/src/components/CommentBox.js @@ -7,13 +7,13 @@ import {Button} from 'react-mdl'; export default class CommentBox extends React.Component { constructor (props) { super(props); - this.state = {name: '', body: '', _csrf: props._csrf}; + this.state = {name: '', body: ''}; this.onSubmit = this.onSubmit.bind(this); } onSubmit () { - const {name, body, _csrf} = this.state; - this.props.onSubmit({name, body, _csrf}); + const {name, body} = this.state; + this.props.onSubmit({name, body}); this.setState({body: '', name: ''}); } diff --git a/client/coral-admin/src/reducers/auth.js b/client/coral-admin/src/reducers/auth.js index 6f3a102de..095ef7aac 100644 --- a/client/coral-admin/src/reducers/auth.js +++ b/client/coral-admin/src/reducers/auth.js @@ -4,8 +4,7 @@ import * as actions from '../constants/auth'; const initialState = Map({ loggedIn: false, user: null, - isAdmin: false, - _csrf: '' + isAdmin: false }); export default function auth (state = initialState, action) { @@ -26,9 +25,6 @@ export default function auth (state = initialState, action) { .set('user', action.user); case actions.LOGOUT_SUCCESS: return initialState; - case actions.CHECK_CSRF_TOKEN: - return state - .set('_csrf', action._csrf); default : return state; } diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 3397a521b..4142c109f 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -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-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 581fb729c..106224c25 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -23,10 +23,9 @@ 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, getState) => { +export const fetchSignIn = (formData) => (dispatch) => { dispatch(signInRequest()); - const _csrf = getState().auth.get('_csrf'); - coralApi('/auth/local', {method: 'POST', body: formData, _csrf}) + coralApi('/auth/local', {method: 'POST', body: formData}) .then(({user}) => { const isAdmin = !!user.roles.filter(i => i === 'admin').length; dispatch(signInSuccess(user, isAdmin)); @@ -73,11 +72,10 @@ 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, getState) => { +export const fetchSignUp = formData => (dispatch) => { dispatch(signUpRequest()); - const _csrf = getState().auth.get('_csrf'); - coralApi('/users', {method: 'POST', body: formData, _csrf}) + coralApi('/users', {method: 'POST', body: formData}) .then(({user}) => { dispatch(signUpSuccess(user)); setTimeout(() =>{ @@ -93,11 +91,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, getState) => { +export const fetchForgotPassword = email => (dispatch) => { dispatch(forgotPassowordRequest(email)); - - const _csrf = getState().auth.get('_csrf'); - coralApi('/users/request-password-reset', {method: 'POST', body: {email}, _csrf}) + coralApi('/account/password/reset', {method: 'POST', body: {email}}) .then(() => dispatch(forgotPassowordSuccess())) .catch(error => dispatch(forgotPassowordFailure(error))); }; @@ -125,15 +121,11 @@ export const invalidForm = error => ({type: actions.INVALID_FORM, error}); const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST}); const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin}); const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); -const checkCSRF = (_csrf) => ({type: actions.CHECK_CSRF_TOKEN, _csrf}); export const checkLogin = () => dispatch => { dispatch(checkLoginRequest()); coralApi('/auth') .then((result) => { - if (result.csrfToken !== null) { - dispatch(checkCSRF(result.csrfToken)); - } if (!result.user) { throw new Error('Not logged in'); } diff --git a/client/coral-framework/actions/items.js b/client/coral-framework/actions/items.js index 303800a8f..48418b2a2 100644 --- a/client/coral-framework/actions/items.js +++ b/client/coral-framework/actions/items.js @@ -190,11 +190,10 @@ export function getItemsArray (ids) { */ export function postItem (item, type, id) { - return (dispatch, getState) => { + return (dispatch) => { if (id) { item.id = id; } - item._csrf = getState().auth.get('_csrf'); return coralApi(`/${type}`, {method: 'POST', body: item}) .then((json) => { dispatch(addItem({...item, id:json.id}, type)); @@ -221,8 +220,7 @@ export function postItem (item, type, id) { */ export function postAction (item_id, item_type, action) { - return (dispatch, getState) => { - action._csrf = getState().auth.get('_csrf'); + 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)); diff --git a/client/coral-framework/actions/user.js b/client/coral-framework/actions/user.js index 2206d306b..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))); }; 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 d8bfd28c4..d612aefb9 100644 --- a/client/coral-framework/helpers/response.js +++ b/client/coral-framework/helpers/response.js @@ -2,19 +2,28 @@ 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); - // Add CSRF field to each POST. - if (options.method.toLowerCase() === 'post' && options._csrf) { - options.body._csrf = options._csrf; + 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') { diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index a003b20ed..36f8e0764 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -11,8 +11,7 @@ const initialState = Map({ error: '', passwordRequestSuccess: null, passwordRequestFailure: null, - successSignUp: false, - _csrf: '' + successSignUp: false }); const purge = user => { 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-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
-