diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx
index 563a8832..6bfcbae7 100644
--- a/client/components/Nav.jsx
+++ b/client/components/Nav.jsx
@@ -29,7 +29,7 @@ function Nav(props) {
if (props.user.authenticated) {
props.saveProject();
} else {
- props.openForceAuthentication();
+ props.showErrorModal('forceAuthentication');
}
}}
>
@@ -168,7 +168,7 @@ Nav.propTypes = {
logoutUser: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired,
showShareModal: PropTypes.func.isRequired,
- openForceAuthentication: PropTypes.func.isRequired
+ showErrorModal: PropTypes.func.isRequired
};
export default Nav;
diff --git a/client/constants.js b/client/constants.js
index 32f24968..baa61b84 100644
--- a/client/constants.js
+++ b/client/constants.js
@@ -106,7 +106,5 @@ export const RESET_JUST_OPENED_PROJECT = 'RESET_JUST_OPENED_PROJECT';
export const SET_PROJECT_SAVED_TIME = 'SET_PROJECT_SAVED_TIME';
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';
+export const SHOW_ERROR_MODAL = 'SHOW_ERROR_MODAL';
+export const HIDE_ERROR_MODAL = 'HIDE_ERROR_MODAL';
diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js
index 487ac392..0d1b0f4c 100644
--- a/client/modules/IDE/actions/ide.js
+++ b/client/modules/IDE/actions/ide.js
@@ -221,26 +221,15 @@ export function setPreviousPath(path) {
};
}
-export function openForceAuthentication() {
+export function showErrorModal(modalType) {
return {
- type: ActionTypes.OPEN_FORCE_AUTHENTICATION
+ type: ActionTypes.SHOW_ERROR_MODAL,
+ modalType
};
}
-export function closeForceAuthentication() {
+export function hideErrorModal() {
return {
- type: ActionTypes.CLOSE_FORCE_AUTHENTICATION
- };
-}
-
-export function showAuthenticationError() {
- return {
- type: ActionTypes.SHOW_AUTHENTICATION_ERROR
- };
-}
-
-export function hideAuthenticationError() {
- return {
- type: ActionTypes.HIDE_AUTHENTICATION_ERROR
+ type: ActionTypes.HIDE_ERROR_MODAL
};
}
diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js
index 385e55c8..0ced170a 100644
--- a/client/modules/IDE/actions/project.js
+++ b/client/modules/IDE/actions/project.js
@@ -7,7 +7,7 @@ import { setUnsavedChanges,
resetJustOpenedProject,
setProjectSavedTime,
resetProjectSavedTime,
- showAuthenticationError } from './ide';
+ showErrorModal } from './ide';
import moment from 'moment';
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
@@ -21,7 +21,6 @@ export function getProject(id) {
}
axios.get(`${ROOT_URL}/projects/${id}`, { withCredentials: true })
.then(response => {
- // browserHistory.push(`/projects/${id}`);
dispatch({
type: ActionTypes.SET_PROJECT,
project: response.data,
@@ -74,7 +73,9 @@ export function saveProject(autosave = false) {
})
.catch((response) => {
if (response.status === 403) {
- dispatch(showAuthenticationError());
+ dispatch(showErrorModal('staleSession'));
+ } else if (response.status === 409) {
+ dispatch(showErrorModal('staleProject'));
} else {
dispatch({
type: ActionTypes.PROJECT_SAVE_FAIL,
@@ -90,8 +91,7 @@ export function saveProject(autosave = false) {
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch({
type: ActionTypes.NEW_PROJECT,
- name: response.data.name,
- id: response.data.id,
+ project: response.data,
owner: response.data.user,
files: response.data.files
});
@@ -109,7 +109,7 @@ export function saveProject(autosave = false) {
})
.catch(response => {
if (response.status === 403) {
- dispatch(showAuthenticationError());
+ dispatch(showErrorModal('staleSession'));
} else {
dispatch({
type: ActionTypes.PROJECT_SAVE_FAIL,
@@ -134,8 +134,7 @@ export function createProject() {
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch({
type: ActionTypes.NEW_PROJECT,
- name: response.data.name,
- id: response.data.id,
+ project: response.data,
owner: response.data.user,
files: response.data.files
});
@@ -176,10 +175,8 @@ export function cloneProject() {
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch({
type: ActionTypes.NEW_PROJECT,
- name: response.data.name,
- id: response.data.id,
+ project: response.data,
owner: response.data.user,
- selectedFile: response.data.selectedFile,
files: response.data.files
});
})
diff --git a/client/modules/IDE/actions/projects.js b/client/modules/IDE/actions/projects.js
index 4c100e35..2561f7c9 100644
--- a/client/modules/IDE/actions/projects.js
+++ b/client/modules/IDE/actions/projects.js
@@ -1,6 +1,6 @@
import * as ActionTypes from '../../../constants';
import axios from 'axios';
-import { showAuthenticationError, setPreviousPath } from './ide';
+import { showErrorModal, setPreviousPath } from './ide';
import { resetProject } from './project';
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
@@ -43,7 +43,7 @@ export function deleteProject(id) {
})
.catch(response => {
if (response.status === 403) {
- dispatch(showAuthenticationError());
+ dispatch(showErrorModal('staleSession'));
} else {
dispatch({
type: ActionTypes.ERROR,
diff --git a/client/modules/IDE/components/AuthenticationError.jsx b/client/modules/IDE/components/AuthenticationError.jsx
deleted file mode 100644
index 6e11a9f0..00000000
--- a/client/modules/IDE/components/AuthenticationError.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React, { PropTypes } from 'react';
-import { Link } from 'react-router';
-
-function AuthenticationError(props) {
- return (
-
-
-
-
- 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/components/ErrorModal.jsx b/client/modules/IDE/components/ErrorModal.jsx
new file mode 100644
index 00000000..7cd91f60
--- /dev/null
+++ b/client/modules/IDE/components/ErrorModal.jsx
@@ -0,0 +1,70 @@
+import React, { PropTypes } from 'react';
+import InlineSVG from 'react-inlinesvg';
+const exitUrl = require('../../../images/exit.svg');
+import { Link } from 'react-router';
+
+class ErrorModal extends React.Component {
+ componentDidMount() {
+ this.refs.modal.focus();
+ }
+
+
+ forceAuthentication() {
+ return (
+
+ In order to save sketches, you must be logged in. Please
+ Login
+ or
+ Sign Up.
+
+ );
+ }
+
+ staleSession() {
+ return (
+
+ It looks like you've been logged out. Please
+ log in.
+
+ );
+ }
+
+ staleProject() {
+ return (
+
+ The project you have attempted to save is out of date. Please refresh the page.
+
+ );
+ }
+
+ render() {
+ return (
+
+
+
+ {(() => { // eslint-disable-line
+ if (this.props.type === 'forceAuthentication') {
+ return this.forceAuthentication();
+ } else if (this.props.type === 'staleSession') {
+ return this.staleSession();
+ } else if (this.props.type === 'staleProject') {
+ return this.staleProject();
+ }
+ })()}
+
+
+ );
+ }
+}
+
+ErrorModal.propTypes = {
+ type: PropTypes.string,
+ closeModal: PropTypes.func.isRequired
+};
+
+export default ErrorModal;
diff --git a/client/modules/IDE/components/ForceAuthentication.jsx b/client/modules/IDE/components/ForceAuthentication.jsx
deleted file mode 100644
index 531c0e70..00000000
--- a/client/modules/IDE/components/ForceAuthentication.jsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React, { PropTypes } from 'react';
-import InlineSVG from 'react-inlinesvg';
-const exitUrl = require('../../../images/exit.svg');
-import { Link } from 'react-router';
-
-class ForceAuthentication extends React.Component {
- componentDidMount() {
- this.refs.forceAuthentication.focus();
- }
-
- render() {
- return (
-
-
-
-
- In order to save sketches, you must be logged in. Please
- Login
- or
- Sign Up.
-
-
-
- );
- }
-}
-
-ForceAuthentication.propTypes = {
- closeModal: PropTypes.func.isRequired
-};
-
-export default ForceAuthentication;
diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx
index ad209a40..5845be81 100644
--- a/client/modules/IDE/pages/IDEView.jsx
+++ b/client/modules/IDE/pages/IDEView.jsx
@@ -9,8 +9,7 @@ import NewFileModal from '../components/NewFileModal';
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 ErrorModal from '../components/ErrorModal';
import Nav from '../../../components/Nav';
import Console from '../components/Console';
import Toast from '../components/Toast';
@@ -195,7 +194,7 @@ class IDEView extends React.Component {
logoutUser={this.props.logoutUser}
stopSketch={this.props.stopSketch}
showShareModal={this.props.showShareModal}
- openForceAuthentication={this.props.openForceAuthentication}
+ showErrorModal={this.props.showErrorModal}
unsavedChanges={this.props.ide.unsavedChanges}
warnIfUnsavedChanges={this.warnIfUnsavedChanges}
/>
@@ -425,22 +424,12 @@ class IDEView extends React.Component {
}
})()}
{(() => { // eslint-disable-line
- if (this.props.ide.forceAuthenticationVisible) {
+ if (this.props.ide.errorType) {
return (
-
-
- );
- }
- })()}
- {(() => { // eslint-disable-line
- if (this.props.ide.authenticationError) {
- return (
-
-
);
@@ -488,9 +477,8 @@ IDEView.propTypes = {
infiniteLoopMessage: PropTypes.string.isRequired,
projectSavedTime: PropTypes.string.isRequired,
previousPath: PropTypes.string.isRequired,
- forceAuthenticationVisible: PropTypes.bool.isRequired,
- authenticationError: PropTypes.bool.isRequired,
- justOpenedProject: PropTypes.bool.isRequired
+ justOpenedProject: PropTypes.bool.isRequired,
+ errorType: PropTypes.string
}).isRequired,
startSketch: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired,
@@ -590,11 +578,10 @@ IDEView.propTypes = {
setBlobUrl: PropTypes.func.isRequired,
setPreviousPath: PropTypes.func.isRequired,
resetProject: PropTypes.func.isRequired,
- closeForceAuthentication: PropTypes.func.isRequired,
- openForceAuthentication: PropTypes.func.isRequired,
console: PropTypes.array.isRequired,
clearConsole: PropTypes.func.isRequired,
- hideAuthenticationError: PropTypes.func.isRequired
+ showErrorModal: PropTypes.func.isRequired,
+ hideErrorModal: PropTypes.func.isRequired
};
function mapStateToProps(state) {
diff --git a/client/modules/IDE/reducers/ide.js b/client/modules/IDE/reducers/ide.js
index 5c01c142..f4908f2a 100644
--- a/client/modules/IDE/reducers/ide.js
+++ b/client/modules/IDE/reducers/ide.js
@@ -19,8 +19,7 @@ const initialState = {
justOpenedProject: false,
projectSavedTime: '',
previousPath: '/',
- forceAuthenticationVisible: false,
- authenticationError: false
+ errorType: undefined
};
const ide = (state = initialState, action) => {
@@ -93,14 +92,10 @@ const ide = (state = initialState, action) => {
return Object.assign({}, state, { projectSavedTime: '' });
case ActionTypes.SET_PREVIOUS_PATH:
return Object.assign({}, state, { previousPath: action.path });
- case ActionTypes.OPEN_FORCE_AUTHENTICATION:
- 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 });
+ case ActionTypes.SHOW_ERROR_MODAL:
+ return Object.assign({}, state, { errorType: action.modalType });
+ case ActionTypes.HIDE_ERROR_MODAL:
+ return Object.assign({}, state, { errorType: undefined });
default:
return state;
}
diff --git a/client/modules/IDE/reducers/project.js b/client/modules/IDE/reducers/project.js
index 7d4df05e..2a89d519 100644
--- a/client/modules/IDE/reducers/project.js
+++ b/client/modules/IDE/reducers/project.js
@@ -18,14 +18,16 @@ const project = (state, action) => {
return Object.assign({}, { ...state }, { name: action.name });
case ActionTypes.NEW_PROJECT:
return {
- id: action.id,
- name: action.name,
+ id: action.project.id,
+ name: action.project.name,
+ updatedAt: action.project.updatedAt,
owner: action.owner
};
case ActionTypes.SET_PROJECT:
return {
id: action.project.id,
name: action.project.name,
+ updatedAt: action.project.updatedAt,
owner: action.owner
};
case ActionTypes.RESET_PROJECT:
diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js
index 3eaeb0fd..21617ac5 100644
--- a/client/modules/User/actions.js
+++ b/client/modules/User/actions.js
@@ -1,7 +1,7 @@
import * as ActionTypes from '../../constants';
import { browserHistory } from 'react-router';
import axios from 'axios';
-import { showAuthenticationError, justOpenedProject } from '../IDE/actions/ide';
+import { showErrorModal, justOpenedProject } from '../IDE/actions/ide';
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
@@ -91,12 +91,12 @@ export function validateSession() {
.then(response => {
const state = getState();
if (state.user.username !== response.data.username) {
- dispatch(showAuthenticationError());
+ dispatch(showErrorModal('staleSession'));
}
})
.catch(response => {
if (response.status === 404) {
- dispatch(showAuthenticationError());
+ dispatch(showErrorModal('staleSession'));
}
});
};
diff --git a/client/styles/components/_authentication-error.scss b/client/styles/components/_authentication-error.scss
deleted file mode 100644
index 5b45e14a..00000000
--- a/client/styles/components/_authentication-error.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-.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/components/_force-authentication.scss b/client/styles/components/_error-modal.scss
similarity index 65%
rename from client/styles/components/_force-authentication.scss
rename to client/styles/components/_error-modal.scss
index b75d87eb..35eb940f 100644
--- a/client/styles/components/_force-authentication.scss
+++ b/client/styles/components/_error-modal.scss
@@ -1,24 +1,24 @@
-.force-authentication {
+.error-modal {
@extend %modal;
display: flex;
flex-wrap: wrap;
flex-flow: column;
}
-.force-authentication__header {
+.error-modal__header {
display: flex;
- justify-content: flex-end;
+ justify-content: space-between;
padding: #{20 / $base-font-size}rem;
}
-.force-authentication__exit-button {
+.error-modal__exit-button {
@include themify() {
@extend %icon;
}
}
-.force-authentication__copy {
+.error-modal__content {
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 823c0e89..d5a507a6 100644
--- a/client/styles/main.scss
+++ b/client/styles/main.scss
@@ -30,10 +30,8 @@
@import 'components/forms';
@import 'components/toast';
@import 'components/timer';
-@import 'components/force-authentication';
@import 'components/form-container';
-@import 'components/uploader';
-@import 'components/authentication-error';
+@import 'components/error-modal';
@import 'layout/ide';
@import 'layout/fullscreen';
diff --git a/server/controllers/project.controller.js b/server/controllers/project.controller.js
index db401e41..0d3c9e9e 100644
--- a/server/controllers/project.controller.js
+++ b/server/controllers/project.controller.js
@@ -2,6 +2,7 @@ import Project from '../models/project';
import User from '../models/user';
import archiver from 'archiver';
import request from 'request';
+import moment from 'moment';
export function createProject(req, res) {
@@ -29,7 +30,10 @@ export function createProject(req, res) {
export function updateProject(req, res) {
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.'});
+ return res.status(403).send({ success: false, message: 'Session does not match owner of project.' });
+ }
+ if (req.body.updatedAt && moment(req.body.updatedAt) < moment(project.updatedAt)) {
+ return res.status(409).send({ success: false, message: 'Attempted to save stale version of project.' })
}
Project.findByIdAndUpdate(req.params.project_id,
{