From 65592cbf9eaed969c2dfa34dd2a02c2f3bf62ec6 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Fri, 13 Jan 2017 17:17:31 -0500 Subject: [PATCH] add authentcation error component, return 403 error from server when trying to save a project where the user doesn't match the owner --- client/constants.js | 2 + client/modules/IDE/actions/ide.js | 12 ++++ client/modules/IDE/actions/project.js | 21 ++++-- .../IDE/components/AuthenticationError.jsx | 24 +++++++ client/modules/IDE/pages/IDEView.jsx | 18 ++++- client/modules/IDE/reducers/ide.js | 5 ++ .../components/_authentication-error.scss | 13 ++++ client/styles/main.scss | 1 + server/controllers/project.controller.js | 72 +++++++++++-------- 9 files changed, 130 insertions(+), 38 deletions(-) create mode 100644 client/modules/IDE/components/AuthenticationError.jsx create mode 100644 client/styles/components/_authentication-error.scss diff --git a/client/constants.js b/client/constants.js index b8c3a389..32f24968 100644 --- a/client/constants.js +++ b/client/constants.js @@ -108,3 +108,5 @@ export const RESET_PROJECT_SAVED_TIME = 'RESET_PROJECT_SAVED_TIME'; export const SET_PREVIOUS_PATH = 'SET_PREVIOUS_PATH'; export const OPEN_FORCE_AUTHENTICATION = 'OPEN_FORCE_AUTHENTICATION'; export const CLOSE_FORCE_AUTHENTICATION = 'CLOSE_FORCE_AUTHENTICATION'; +export const SHOW_AUTHENTICATION_ERROR = 'SHOW_AUTHENTICATION_ERROR'; +export const HIDE_AUTHENTICATION_ERROR = 'HIDE_AUTHENTICATION_ERROR'; diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js index f66ef395..487ac392 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -232,3 +232,15 @@ export function closeForceAuthentication() { type: ActionTypes.CLOSE_FORCE_AUTHENTICATION }; } + +export function showAuthenticationError() { + return { + type: ActionTypes.SHOW_AUTHENTICATION_ERROR + }; +} + +export function hideAuthenticationError() { + return { + type: ActionTypes.HIDE_AUTHENTICATION_ERROR + }; +} diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index 020cc921..2f17bf1a 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -2,7 +2,12 @@ import * as ActionTypes from '../../../constants'; import { browserHistory } from 'react-router'; import axios from 'axios'; import { showToast, setToastText } from './toast'; -import { setUnsavedChanges, justOpenedProject, resetJustOpenedProject, setProjectSavedTime, resetProjectSavedTime } from './ide'; +import { setUnsavedChanges, + justOpenedProject, + resetJustOpenedProject, + setProjectSavedTime, + resetProjectSavedTime, + showAuthenticationError } from './ide'; import moment from 'moment'; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; @@ -67,10 +72,16 @@ export function saveProject(autosave = false) { } } }) - .catch((response) => dispatch({ - type: ActionTypes.PROJECT_SAVE_FAIL, - error: response.data - })); + .catch((response) => { + if (response.status === 403) { + dispatch(showAuthenticationError()); + } else { + dispatch({ + type: ActionTypes.PROJECT_SAVE_FAIL, + error: response.data + }); + } + }); } else { axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true }) .then(response => { diff --git a/client/modules/IDE/components/AuthenticationError.jsx b/client/modules/IDE/components/AuthenticationError.jsx new file mode 100644 index 00000000..6e11a9f0 --- /dev/null +++ b/client/modules/IDE/components/AuthenticationError.jsx @@ -0,0 +1,24 @@ +import React, { PropTypes } from 'react'; +import { Link } from 'react-router'; + +function AuthenticationError(props) { + return ( +
+
+

Error

+
+
+

+ It looks like you've been logged out. Please  + log in. +

+
+
+ ); +} + +AuthenticationError.propTypes = { + closeModal: PropTypes.func.isRequired +}; + +export default AuthenticationError; diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index fc552603..a5ff6000 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -10,6 +10,7 @@ import NewFolderModal from '../components/NewFolderModal'; import ShareModal from '../components/ShareModal'; import KeyboardShortcutModal from '../components/KeyboardShortcutModal'; import ForceAuthentication from '../components/ForceAuthentication'; +import AuthenticationError from '../components/AuthenticationError'; import Nav from '../../../components/Nav'; import Console from '../components/Console'; import Toast from '../components/Toast'; @@ -446,6 +447,17 @@ class IDEView extends React.Component { ); } })()} + {(() => { // eslint-disable-line + if (this.props.ide.authenticationError) { + return ( + + + + ); + } + })()} ); @@ -488,7 +500,8 @@ IDEView.propTypes = { infiniteLoopMessage: PropTypes.string.isRequired, projectSavedTime: PropTypes.string.isRequired, previousPath: PropTypes.string.isRequired, - forceAuthenticationVisible: PropTypes.bool.isRequired + forceAuthenticationVisible: PropTypes.bool.isRequired, + authenticationError: PropTypes.bool.isRequired }).isRequired, startSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired, @@ -591,7 +604,8 @@ IDEView.propTypes = { closeForceAuthentication: PropTypes.func.isRequired, openForceAuthentication: PropTypes.func.isRequired, console: PropTypes.array.isRequired, - clearConsole: PropTypes.func.isRequired + clearConsole: PropTypes.func.isRequired, + hideAuthenticationError: PropTypes.func.isRequired }; function mapStateToProps(state) { diff --git a/client/modules/IDE/reducers/ide.js b/client/modules/IDE/reducers/ide.js index 007ace9e..f77ef048 100644 --- a/client/modules/IDE/reducers/ide.js +++ b/client/modules/IDE/reducers/ide.js @@ -20,6 +20,7 @@ const initialState = { projectSavedTime: '', previousPath: '/', forceAuthenticationVisible: false, + authenticationError: false }; const ide = (state = initialState, action) => { @@ -96,6 +97,10 @@ const ide = (state = initialState, action) => { return Object.assign({}, state, { forceAuthenticationVisible: true }); case ActionTypes.CLOSE_FORCE_AUTHENTICATION: return Object.assign({}, state, { forceAuthenticationVisible: false }); + case ActionTypes.SHOW_AUTHENTICATION_ERROR: + return Object.assign({}, state, { authenticationError: true }); + case ActionTypes.HIDE_AUTHENTICATION_ERROR: + return Object.assign({}, state, { authenticationError: false }); default: return state; } diff --git a/client/styles/components/_authentication-error.scss b/client/styles/components/_authentication-error.scss new file mode 100644 index 00000000..5b45e14a --- /dev/null +++ b/client/styles/components/_authentication-error.scss @@ -0,0 +1,13 @@ +.authentication-error { + @extend %modal; +} + +.authentication-error__header { + padding: #{20 / $base-font-size}rem; +} + +.authentication-error__copy { + padding: #{20 / $base-font-size}rem; + padding-top: 0; + padding-bottom: #{60 / $base-font-size}rem; +} \ No newline at end of file diff --git a/client/styles/main.scss b/client/styles/main.scss index c5ff8191..823c0e89 100644 --- a/client/styles/main.scss +++ b/client/styles/main.scss @@ -33,6 +33,7 @@ @import 'components/force-authentication'; @import 'components/form-container'; @import 'components/uploader'; +@import 'components/authentication-error'; @import 'layout/ide'; @import 'layout/fullscreen'; diff --git a/server/controllers/project.controller.js b/server/controllers/project.controller.js index 5e758f3d..0f75ee3c 100644 --- a/server/controllers/project.controller.js +++ b/server/controllers/project.controller.js @@ -23,33 +23,38 @@ export function createProject(req, res) { } export function updateProject(req, res) { - Project.findByIdAndUpdate(req.params.project_id, - { - $set: req.body - }) - .populate('user', 'username') - .exec((err, updatedProject) => { - if (err) { - console.log(err); - return res.json({ success: false }); - } - if (updatedProject.files.length !== req.body.files.length) { - const oldFileIds = updatedProject.files.map(file => file.id); - const newFileIds = req.body.files.map(file => file.id); - const staleIds = oldFileIds.filter(id => newFileIds.indexOf(id) === -1); - staleIds.forEach(staleId => { - updatedProject.files.id(staleId).remove(); - }); - updatedProject.save((innerErr) => { - if (innerErr) { - console.log(innerErr); - return res.json({ success: false }); - } - return res.json(updatedProject); - }); - } - return res.json(updatedProject); - }); + Project.findById(req.params.project_id, (err, project) => { + if (!req.user || !project.user.equals(req.user._id)) { + return res.status(403).send({ success: false, message: 'Session does not match owner of project.'}); + } + Project.findByIdAndUpdate(req.params.project_id, + { + $set: req.body + }) + .populate('user', 'username') + .exec((err, updatedProject) => { + if (err) { + console.log(err); + return res.json({ success: false }); + } + if (updatedProject.files.length !== req.body.files.length) { + const oldFileIds = updatedProject.files.map(file => file.id); + const newFileIds = req.body.files.map(file => file.id); + const staleIds = oldFileIds.filter(id => newFileIds.indexOf(id) === -1); + staleIds.forEach(staleId => { + updatedProject.files.id(staleId).remove(); + }); + updatedProject.save((innerErr) => { + if (innerErr) { + console.log(innerErr); + return res.json({ success: false }); + } + return res.json(updatedProject); + }); + } + return res.json(updatedProject); + }); + }); } export function getProject(req, res) { @@ -64,11 +69,16 @@ export function getProject(req, res) { } export function deleteProject(req, res) { - Project.remove({ _id: req.params.project_id }, (err) => { - if (err) { - return res.status(404).send({ message: 'Project with that id does not exist' }); + Project.findById(req.params.project_id, (err, project) => { + if (!req.user || !project.user.equals(req.user._id)) { + return res.status(403).json({ success: false, message: 'Session does not match owner of project.'}); } - return res.json({ success: true }); + Project.remove({ _id: req.params.project_id }, (err) => { + if (err) { + return res.status(404).send({ message: 'Project with that id does not exist' }); + } + return res.json({ success: true }); + }); }); }