From 82207a50d39d87872c257a441609baef8f817b9d Mon Sep 17 00:00:00 2001 From: Mathura MG Date: Wed, 31 May 2017 12:23:30 -0700 Subject: [PATCH 01/24] Accessibility (#361) * add p5 interceptor submodule * update package * remoce interceptor * update interceptor; * merge scripts * change postinstall script * refactor interceptor files * remove merge conflicts * change source files * add registry class * provide seperate outputs for text and grid * switch textOutput to boolean * make both modules usable together * update interceptor for safari * fix grid label * add sound output as well * change file strucure * change constants * change input lables * switch submodule branch * change variable name * change grid to table * remove role from table elements * switch submodule branch --- client/constants.js | 6 +- client/modules/IDE/actions/ide.js | 8 +-- client/modules/IDE/actions/preferences.js | 37 +++++++++- .../IDE/components/AccessibleOutput.jsx | 51 ++++++++++++++ client/modules/IDE/components/GridOutput.jsx | 38 ++++++++++ client/modules/IDE/components/Preferences.jsx | 55 +++++++-------- .../modules/IDE/components/PreviewFrame.jsx | 69 +++++++++++-------- client/modules/IDE/components/TextOutput.jsx | 31 ++------- client/modules/IDE/components/Toolbar.jsx | 8 +-- client/modules/IDE/pages/IDEView.jsx | 48 ++++++++----- client/modules/IDE/reducers/ide.js | 10 +-- client/modules/IDE/reducers/preferences.js | 8 ++- client/styles/components/_preferences.scss | 6 +- client/styles/layout/_ide.scss | 2 +- server/models/user.js | 4 +- static/p5-interceptor | 2 +- 16 files changed, 261 insertions(+), 122 deletions(-) create mode 100644 client/modules/IDE/components/AccessibleOutput.jsx create mode 100644 client/modules/IDE/components/GridOutput.jsx diff --git a/client/constants.js b/client/constants.js index 149dc79c..8f1a5573 100644 --- a/client/constants.js +++ b/client/constants.js @@ -4,8 +4,8 @@ export const TOGGLE_SKETCH = 'TOGGLE_SKETCH'; export const START_SKETCH = 'START_SKETCH'; export const STOP_SKETCH = 'STOP_SKETCH'; -export const START_TEXT_OUTPUT = 'START_TEXT_OUTPUT'; -export const STOP_TEXT_OUTPUT = 'STOP_TEXT_OUTPUT'; +export const START_ACCESSIBLE_OUTPUT = 'START_ACCESSIBLE_OUTPUT'; +export const STOP_ACCESSIBLE_OUTPUT = 'STOP_ACCESSIBLE_OUTPUT'; export const OPEN_PREFERENCES = 'OPEN_PREFERENCES'; export const CLOSE_PREFERENCES = 'CLOSE_PREFERENCES'; @@ -69,6 +69,8 @@ export const SET_AUTOSAVE = 'SET_AUTOSAVE'; export const SET_LINT_WARNING = 'SET_LINT_WARNING'; export const SET_PREFERENCES = 'SET_PREFERENCES'; export const SET_TEXT_OUTPUT = 'SET_TEXT_OUTPUT'; +export const SET_GRID_OUTPUT = 'SET_GRID_OUTPUT'; +export const SET_SOUND_OUTPUT = 'SET_SOUND_OUTPUT'; export const OPEN_PROJECT_OPTIONS = 'OPEN_PROJECT_OPTIONS'; export const CLOSE_PROJECT_OPTIONS = 'CLOSE_PROJECT_OPTIONS'; diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js index 4f5c0662..0e8fa3ae 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -31,15 +31,15 @@ export function endSketchRefresh() { }; } -export function startTextOutput() { +export function startAccessibleOutput() { return { - type: ActionTypes.START_TEXT_OUTPUT + type: ActionTypes.START_ACCESSIBLE_OUTPUT }; } -export function stopTextOutput() { +export function stopAccessibleOutput() { return { - type: ActionTypes.STOP_TEXT_OUTPUT + type: ActionTypes.STOP_ACCESSIBLE_OUTPUT }; } diff --git a/client/modules/IDE/actions/preferences.js b/client/modules/IDE/actions/preferences.js index bdc40f76..4f735280 100644 --- a/client/modules/IDE/actions/preferences.js +++ b/client/modules/IDE/actions/preferences.js @@ -137,6 +137,42 @@ export function setTextOutput(value) { }; } +export function setGridOutput(value) { + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.SET_GRID_OUTPUT, + value + }); + const state = getState(); + if (state.user.authenticated) { + const formParams = { + preferences: { + gridOutput: value + } + }; + updatePreferences(formParams, dispatch); + } + }; +} + +export function setSoundOutput(value) { + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.SET_SOUND_OUTPUT, + value + }); + const state = getState(); + if (state.user.authenticated) { + const formParams = { + preferences: { + soundOutput: value + } + }; + updatePreferences(formParams, dispatch); + } + }; +} + export function setTheme(value) { // return { // type: ActionTypes.SET_THEME, @@ -180,4 +216,3 @@ export function setAutorefresh(value) { } }; } - diff --git a/client/modules/IDE/components/AccessibleOutput.jsx b/client/modules/IDE/components/AccessibleOutput.jsx new file mode 100644 index 00000000..a3688bd7 --- /dev/null +++ b/client/modules/IDE/components/AccessibleOutput.jsx @@ -0,0 +1,51 @@ +import React, { PropTypes } from 'react'; +import GridOutput from '../components/GridOutput'; +import TextOutput from '../components/TextOutput'; + +class AccessibleOutput extends React.Component { + componentDidMount() { + this.accessibleOutputModal.focus(); + } + componentDidUpdate(prevProps) { + // if the user explicitly clicks on the play button, want to refocus on the text output + if (this.props.isPlaying && this.props.previewIsRefreshing) { + this.accessibleOutputModal.focus(); + } + } + render() { + return ( +
{ this.accessibleOutputModal = element; }} + tabIndex="0" + aria-label="accessible-output" + title="canvas text output" + > + {(() => { // eslint-disable-line + if (this.props.textOutput) { + return ( + + ); + } + })()} + {(() => { // eslint-disable-line + if (this.props.gridOutput) { + return ( + + ); + } + })()} +
+ ); + } +} + +AccessibleOutput.propTypes = { + isPlaying: PropTypes.bool.isRequired, + previewIsRefreshing: PropTypes.bool.isRequired, + textOutput: PropTypes.bool.isRequired, + gridOutput: PropTypes.bool.isRequired +}; + +export default AccessibleOutput; diff --git a/client/modules/IDE/components/GridOutput.jsx b/client/modules/IDE/components/GridOutput.jsx new file mode 100644 index 00000000..17fb1213 --- /dev/null +++ b/client/modules/IDE/components/GridOutput.jsx @@ -0,0 +1,38 @@ +import React, { PropTypes } from 'react'; + +class GridOutput extends React.Component { + componentDidMount() { + this.GridOutputModal.focus(); + } + render() { + return ( +
{ this.GridOutputModal = element; }} + > +

Grid Output

+

+

+ +
+
+
+
+ ); + } +} + +export default GridOutput; diff --git a/client/modules/IDE/components/Preferences.jsx b/client/modules/IDE/components/Preferences.jsx index cb549a13..24203322 100644 --- a/client/modules/IDE/components/Preferences.jsx +++ b/client/modules/IDE/components/Preferences.jsx @@ -261,50 +261,41 @@ class Preferences extends React.Component {
this.props.setTextOutput(1)} + type="checkbox" + onChange={(event) => { + this.props.setTextOutput(event.target.checked); + }} aria-label="text output on" name="text output" id="text-output-on" - className="preference__radio-button" value="On" - checked={Boolean(this.props.textOutput === 1)} + checked={(this.props.textOutput)} /> this.props.setTextOutput(2)} - aria-label="table text output on" - name="table text output" - id="grid-output-on" - className="preference__radio-button" - value="Grid On" - checked={Boolean(this.props.textOutput === 2)} + type="checkbox" + onChange={(event) => { + this.props.setGridOutput(event.target.checked); + }} + aria-label="table output on" + name="table output" + id="table-output-on" + value="On" + checked={(this.props.gridOutput)} /> - + this.props.setTextOutput(3)} + type="checkbox" + onChange={(event) => { + this.props.setSoundOutput(event.target.checked); + }} aria-label="sound output on" name="sound output" id="sound-output-on" - className="preference__radio-button" value="On" - checked={Boolean(this.props.textOutput === 3)} + checked={(this.props.soundOutput)} /> - this.props.setTextOutput(0)} - aria-label="text output off" - name="text output" - id="text-output-off" - className="preference__radio-button" - value="Off" - checked={!(this.props.textOutput)} - /> - -
@@ -324,8 +315,12 @@ Preferences.propTypes = { setFontSize: PropTypes.func.isRequired, autosave: PropTypes.bool.isRequired, setAutosave: PropTypes.func.isRequired, - textOutput: PropTypes.number.isRequired, + textOutput: PropTypes.bool.isRequired, + gridOutput: PropTypes.bool.isRequired, + soundOutput: PropTypes.bool.isRequired, setTextOutput: PropTypes.func.isRequired, + setGridOutput: PropTypes.func.isRequired, + setSoundOutput: PropTypes.func.isRequired, lintWarning: PropTypes.bool.isRequired, setLintWarning: PropTypes.func.isRequired, theme: PropTypes.string.isRequired, diff --git a/client/modules/IDE/components/PreviewFrame.jsx b/client/modules/IDE/components/PreviewFrame.jsx index 33af7507..622e7329 100644 --- a/client/modules/IDE/components/PreviewFrame.jsx +++ b/client/modules/IDE/components/PreviewFrame.jsx @@ -114,7 +114,7 @@ class PreviewFrame extends React.Component { } // if user switches textoutput preferences - if (this.props.isTextOutputPlaying !== prevProps.isTextOutputPlaying) { + if (this.props.isAccessibleOutputPlaying !== prevProps.isAccessibleOutputPlaying) { this.renderSketch(); return; } @@ -124,6 +124,16 @@ class PreviewFrame extends React.Component { return; } + if (this.props.gridOutput !== prevProps.gridOutput) { + this.renderSketch(); + return; + } + + if (this.props.soundOutput !== prevProps.soundOutput) { + this.renderSketch(); + return; + } + if (this.props.fullView && this.props.files[0].id !== prevProps.files[0].id) { this.renderSketch(); } @@ -165,38 +175,41 @@ class PreviewFrame extends React.Component { '/loop-protect.min.js', '/hijackConsole.js' ]; - if (this.props.isTextOutputPlaying || (this.props.textOutput !== 0 && this.props.isPlaying)) { + if (this.props.isAccessibleOutputPlaying || ((this.props.textOutput || this.props.gridOutput || this.props.soundOutput) && this.props.isPlaying)) { let interceptorScripts = []; - if (this.props.textOutput === 0) { - this.props.setTextOutput(1); + interceptorScripts = [ + '/p5-interceptor/registry.js', + '/p5-interceptor/loadData.js', + '/p5-interceptor/interceptorHelperFunctions.js', + '/p5-interceptor/baseInterceptor.js', + '/p5-interceptor/entities/entity.min.js', + '/p5-interceptor/ntc.min.js' + ]; + if (!this.props.textOutput && !this.props.gridOutput && !this.props.soundOutput) { + this.props.setTextOutput(true); } - if (this.props.textOutput === 1) { - interceptorScripts = [ - '/p5-interceptor/registry.js', - '/p5-interceptor/loadData.js', - '/p5-interceptor/interceptorHelperFunctions.js', - '/p5-interceptor/baseInterceptor.js', - '/p5-interceptor/entities/entity.min.js', + if (this.props.textOutput) { + let textInterceptorScripts = []; + textInterceptorScripts = [ '/p5-interceptor/textInterceptor/interceptorFunctions.js', - '/p5-interceptor/textInterceptor/interceptorP5.js', - '/p5-interceptor/ntc.min.js' + '/p5-interceptor/textInterceptor/interceptorP5.js' ]; - } else if (this.props.textOutput === 2) { - interceptorScripts = [ - '/p5-interceptor/registry.js', - '/p5-interceptor/loadData.js', - '/p5-interceptor/interceptorHelperFunctions.js', - '/p5-interceptor/baseInterceptor.js', - '/p5-interceptor/entities/entity.min.js', + interceptorScripts = interceptorScripts.concat(textInterceptorScripts); + } + if (this.props.gridOutput) { + let gridInterceptorScripts = []; + gridInterceptorScripts = [ '/p5-interceptor/gridInterceptor/interceptorFunctions.js', - '/p5-interceptor/gridInterceptor/interceptorP5.js', - '/p5-interceptor/ntc.min.js' + '/p5-interceptor/gridInterceptor/interceptorP5.js' ]; - } else if (this.props.textOutput === 3) { - interceptorScripts = [ - '/p5-interceptor/loadData.js', + interceptorScripts = interceptorScripts.concat(gridInterceptorScripts); + } + if (this.props.soundOutput) { + let soundInterceptorScripts = []; + soundInterceptorScripts = [ '/p5-interceptor/soundInterceptor/interceptorP5.js' ]; + interceptorScripts = interceptorScripts.concat(soundInterceptorScripts); } scriptsToInject = scriptsToInject.concat(interceptorScripts); } @@ -373,8 +386,10 @@ class PreviewFrame extends React.Component { PreviewFrame.propTypes = { isPlaying: PropTypes.bool.isRequired, - isTextOutputPlaying: PropTypes.bool.isRequired, - textOutput: PropTypes.number.isRequired, + isAccessibleOutputPlaying: PropTypes.bool.isRequired, + textOutput: PropTypes.bool.isRequired, + gridOutput: PropTypes.bool.isRequired, + soundOutput: PropTypes.bool.isRequired, setTextOutput: PropTypes.func.isRequired, htmlFile: PropTypes.shape({ content: PropTypes.string.isRequired diff --git a/client/modules/IDE/components/TextOutput.jsx b/client/modules/IDE/components/TextOutput.jsx index 67d67db6..f9b528e0 100644 --- a/client/modules/IDE/components/TextOutput.jsx +++ b/client/modules/IDE/components/TextOutput.jsx @@ -1,28 +1,16 @@ -import React, { PropTypes } from 'react'; +import React from 'react'; class TextOutput extends React.Component { componentDidMount() { - this.canvasTextOutput.focus(); - } - componentDidUpdate(prevProps) { - // if the user explicitly clicks on the play button, want to refocus on the text output - if (this.props.isPlaying && this.props.previewIsRefreshing) { - this.canvasTextOutput.focus(); - } + this.TextOutputModal.focus(); } render() { return (
{ this.canvasTextOutput = element; }} - tabIndex="0" - aria-label="text-output" - title="canvas text output" + id="textOutput-content" + ref={(element) => { this.TextOutputModal = element; }} > -

Output

-
-
+

Text Output

{ this.props.clearConsole(); - this.props.startTextOutput(); + this.props.startAccessibleOutput(); this.props.startSketchAndRefresh(); }} aria-label="play sketch" @@ -85,7 +85,7 @@ class Toolbar extends React.Component {
{(() => { - if ((this.props.preferences.textOutput && this.props.ide.isPlaying) || this.props.ide.isTextOutputPlaying) { + if (((this.props.preferences.textOutput || this.props.preferences.gridOutput || this.props.preferences.soundOutput) && this.props.ide.isPlaying) || this.props.ide.isAccessibleOutputPlaying) { return ( - ); } @@ -362,9 +368,13 @@ class IDEView extends React.Component { files={this.props.files} content={this.props.selectedFile.content} isPlaying={this.props.ide.isPlaying} - isTextOutputPlaying={this.props.ide.isTextOutputPlaying} + isAccessibleOutputPlaying={this.props.ide.isAccessibleOutputPlaying} textOutput={this.props.preferences.textOutput} + gridOutput={this.props.preferences.gridOutput} + soundOutput={this.props.preferences.soundOutput} setTextOutput={this.props.setTextOutput} + setGridOutput={this.props.setGridOutput} + setSoundOutput={this.props.setSoundOutput} dispatchConsoleEvent={this.props.dispatchConsoleEvent} autorefresh={this.props.preferences.autorefresh} previewIsRefreshing={this.props.ide.previewIsRefreshing} @@ -494,7 +504,7 @@ IDEView.propTypes = { saveProject: PropTypes.func.isRequired, ide: PropTypes.shape({ isPlaying: PropTypes.bool.isRequired, - isTextOutputPlaying: PropTypes.bool.isRequired, + isAccessibleOutputPlaying: PropTypes.bool.isRequired, consoleEvent: PropTypes.array, modalIsVisible: PropTypes.bool.isRequired, sidebarIsExpanded: PropTypes.bool.isRequired, @@ -516,8 +526,8 @@ IDEView.propTypes = { helpType: PropTypes.string }).isRequired, stopSketch: PropTypes.func.isRequired, - startTextOutput: PropTypes.func.isRequired, - stopTextOutput: PropTypes.func.isRequired, + startAccessibleOutput: PropTypes.func.isRequired, + stopAccessibleOutput: PropTypes.func.isRequired, project: PropTypes.shape({ id: PropTypes.string, name: PropTypes.string.isRequired, @@ -542,7 +552,9 @@ IDEView.propTypes = { isTabIndent: PropTypes.bool.isRequired, autosave: PropTypes.bool.isRequired, lintWarning: PropTypes.bool.isRequired, - textOutput: PropTypes.number.isRequired, + textOutput: PropTypes.bool.isRequired, + gridOutput: PropTypes.bool.isRequired, + soundOutput: PropTypes.bool.isRequired, theme: PropTypes.string.isRequired, autorefresh: PropTypes.bool.isRequired }).isRequired, @@ -554,6 +566,8 @@ IDEView.propTypes = { setAutosave: PropTypes.func.isRequired, setLintWarning: PropTypes.func.isRequired, setTextOutput: PropTypes.func.isRequired, + setGridOutput: PropTypes.func.isRequired, + setSoundOutput: PropTypes.func.isRequired, files: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, diff --git a/client/modules/IDE/reducers/ide.js b/client/modules/IDE/reducers/ide.js index 6ad8bf1f..30d0b553 100644 --- a/client/modules/IDE/reducers/ide.js +++ b/client/modules/IDE/reducers/ide.js @@ -2,7 +2,7 @@ import * as ActionTypes from '../../../constants'; const initialState = { isPlaying: false, - isTextOutputPlaying: false, + isAccessibleOutputPlaying: false, modalIsVisible: false, sidebarIsExpanded: false, consoleIsExpanded: true, @@ -27,10 +27,10 @@ const ide = (state = initialState, action) => { return Object.assign({}, state, { isPlaying: true }); case ActionTypes.STOP_SKETCH: return Object.assign({}, state, { isPlaying: false }); - case ActionTypes.START_TEXT_OUTPUT: - return Object.assign({}, state, { isTextOutputPlaying: true }); - case ActionTypes.STOP_TEXT_OUTPUT: - return Object.assign({}, state, { isTextOutputPlaying: false }); + case ActionTypes.START_ACCESSIBLE_OUTPUT: + return Object.assign({}, state, { isAccessibleOutputPlaying: true }); + case ActionTypes.STOP_ACCESSIBLE_OUTPUT: + return Object.assign({}, state, { isAccessibleOutputPlaying: false }); case ActionTypes.CONSOLE_EVENT: return Object.assign({}, state, { consoleEvent: action.event }); case ActionTypes.SHOW_MODAL: diff --git a/client/modules/IDE/reducers/preferences.js b/client/modules/IDE/reducers/preferences.js index f4161670..8441f042 100644 --- a/client/modules/IDE/reducers/preferences.js +++ b/client/modules/IDE/reducers/preferences.js @@ -6,7 +6,9 @@ const initialState = { isTabIndent: true, autosave: true, lintWarning: false, - textOutput: 0, + textOutput: false, + gridOutput: false, + soundOutput: false, theme: 'light', autorefresh: false }; @@ -31,6 +33,10 @@ const preferences = (state = initialState, action) => { return Object.assign({}, state, { lintWarning: action.value }); case ActionTypes.SET_TEXT_OUTPUT: return Object.assign({}, state, { textOutput: action.value }); + case ActionTypes.SET_GRID_OUTPUT: + return Object.assign({}, state, { gridOutput: action.value }); + case ActionTypes.SET_SOUND_OUTPUT: + return Object.assign({}, state, { soundOutput: action.value }); case ActionTypes.SET_PREFERENCES: return action.preferences; case ActionTypes.SET_THEME: diff --git a/client/styles/components/_preferences.scss b/client/styles/components/_preferences.scss index e684cd96..f4938d3f 100644 --- a/client/styles/components/_preferences.scss +++ b/client/styles/components/_preferences.scss @@ -126,7 +126,7 @@ } .preference__option:last-child { - padding-right: 0; + padding-right: 0; } .preference__preview-button { @@ -151,5 +151,5 @@ } .preference__option.preference__canvas:not(:last-child) { - padding-right: #{20 / $base-font-size}rem; -} \ No newline at end of file + padding-right: #{14 / $base-font-size}rem; +} diff --git a/client/styles/layout/_ide.scss b/client/styles/layout/_ide.scss index 87e3724c..cb446951 100644 --- a/client/styles/layout/_ide.scss +++ b/client/styles/layout/_ide.scss @@ -31,7 +31,7 @@ @extend %hidden-element; } -.text-output { +.accessible-output { @extend %hidden-element; } diff --git a/server/models/user.js b/server/models/user.js index cd43c7c9..0f89cb15 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -19,7 +19,9 @@ const userSchema = new Schema({ isTabIndent: { type: Boolean, default: false }, autosave: { type: Boolean, default: true }, lintWarning: { type: Boolean, default: false }, - textOutput: { type: Number, default: 0 }, + textOutput: { type: Boolean, default: false }, + gridOutput: { type: Boolean, default: false }, + soundOutput: { type: Boolean, default: false }, theme: { type: String, default: 'light' }, autorefresh: { type: Boolean, default: false } } diff --git a/static/p5-interceptor b/static/p5-interceptor index 344fedf8..5b924f34 160000 --- a/static/p5-interceptor +++ b/static/p5-interceptor @@ -1 +1 @@ -Subproject commit 344fedf8d868c62adc571bf4212c6bea3cd20247 +Subproject commit 5b924f3460886b82d72d0ab0d709f323f8b9a588 From 313fc856d19a49714dbd692fe052893bf98f1859 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 6 Jun 2017 04:33:32 +0200 Subject: [PATCH 02/24] Fixes linting errors (#362) --- client/components/Nav.jsx | 15 ++++++++++++--- client/modules/IDE/components/Console.jsx | 6 +++++- client/modules/IDE/components/FileNode.jsx | 6 +++++- client/modules/IDE/components/GridOutput.jsx | 2 +- client/modules/IDE/components/PreviewFrame.jsx | 6 +++++- client/modules/IDE/components/Toolbar.jsx | 5 ++++- client/modules/IDE/pages/IDEView.jsx | 12 ++++++++++-- client/modules/User/components/AccountForm.jsx | 13 +++++++++++-- .../modules/User/components/NewPasswordForm.jsx | 6 +++++- .../modules/User/components/ResetPasswordForm.jsx | 7 ++++++- client/modules/User/components/SignupForm.jsx | 6 +++++- client/utils/reduxFormUtils.js | 4 +++- 12 files changed, 72 insertions(+), 16 deletions(-) diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index 181c0e64..6ef4fb35 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -21,7 +21,10 @@ class Nav extends React.PureComponent { {(() => { // eslint-disable-line - if (!this.props.project.owner || (this.props.project.owner && this.props.project.owner.id === this.props.user.id)) { + if ( + !this.props.project.owner || + (this.props.project.owner && this.props.project.owner.id === this.props.user.id) + ) { return (
  • ); @@ -153,7 +158,11 @@ class Nav extends React.PureComponent {
    This is a preview version of the editor, that has not yet been officially released. - It is in development, you can report bugs here. + It is in development, you can report bugs here. Please use with caution.
    diff --git a/client/modules/IDE/components/Console.jsx b/client/modules/IDE/components/Console.jsx index d3f2eabb..6fb3e143 100644 --- a/client/modules/IDE/components/Console.jsx +++ b/client/modules/IDE/components/Console.jsx @@ -24,7 +24,11 @@ class Console extends React.Component { - + + +
    +
    + + +
    + + + `; + + function startSearch(cm, state, originalQuery) { + state.queryText = originalQuery; + state.query = parseQuery(originalQuery, state); + + cm.removeOverlay(state.overlay, state.caseInsensitive); + state.overlay = searchOverlay(state.query); + cm.addOverlay(state.overlay); + if (cm.showMatchesOnScrollbar) { + if (state.annotate) { state.annotate.clear(); state.annotate = null; } + state.annotate = cm.showMatchesOnScrollbar(state.query, state.caseInsensitive); + } + } + + function doSearch(cm, rev, persistent, immediate, ignoreQuery) { + var state = getSearchState(cm); + if (!ignoreQuery && state.query) return findNext(cm, rev); + var q = cm.getSelection() || state.lastQuery; + if (persistent && cm.openDialog) { + var hiding = null + var searchNext = function(query, event) { + CodeMirror.e_stop(event); + if (!query) return; + if (query != state.queryText) { + startSearch(cm, state, query); + state.posFrom = state.posTo = cm.getCursor(); + } + if (hiding) hiding.style.opacity = 1 + findNext(cm, event.shiftKey, function(_, to) { + var dialog + if (to.line < 3 && document.querySelector && + (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) && + dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top) + (hiding = dialog).style.opacity = .4 + }) + }; + persistentDialog(cm, queryDialog, q, searchNext, function(event, query) { + var keyName = CodeMirror.keyName(event) + var cmd = CodeMirror.keyMap[cm.getOption("keyMap")][keyName] + if (!cmd) cmd = cm.getOption('extraKeys')[keyName] + if (cmd == "findNext" || cmd == "findPrev" || + cmd == "findPersistentNext" || cmd == "findPersistentPrev") { + CodeMirror.e_stop(event); + startSearch(cm, getSearchState(cm), query); + cm.execCommand(cmd); + } else if (cmd == "find" || cmd == "findPersistent") { + CodeMirror.e_stop(event); + searchNext(query, event); + } + }); + if (immediate && q) { + startSearch(cm, state, q); + findNext(cm, rev); + } + } else { + dialog(cm, queryDialog, "Search for:", q, function(query) { + if (query && !state.query) cm.operation(function() { + startSearch(cm, state, query); + state.posFrom = state.posTo = cm.getCursor(); + findNext(cm, rev); + }); + }); + } + } + + function findNext(cm, rev, callback) {cm.operation(function() { + var state = getSearchState(cm); + var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo); + if (!cursor.find(rev)) { + cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0)); + if (!cursor.find(rev)) return; + } + cm.setSelection(cursor.from(), cursor.to()); + cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 60); + state.posFrom = cursor.from(); state.posTo = cursor.to(); + if (callback) callback(cursor.from(), cursor.to()) + });} + + function clearSearch(cm) {cm.operation(function() { + var state = getSearchState(cm); + state.lastQuery = state.queryText; + if (!state.query) return; + state.query = state.queryText = null; + cm.removeOverlay(state.overlay); + if (state.annotate) { state.annotate.clear(); state.annotate = null; } + });} + + var replaceQueryDialog = + '
    '; + + var replacementQueryDialog = 'With: '; + var doReplaceConfirm = "Replace? "; + + function replaceAll(cm, query, text) { + cm.operation(function() { + for (var cursor = getSearchCursor(cm, query); cursor.findNext();) { + if (typeof query != "string") { + var match = cm.getRange(cursor.from(), cursor.to()).match(query); + cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];})); + } else cursor.replace(text); + } + }); + } + + // TODO: This will need updating if replace is implemented + function replace(cm, all) { + var prevDialog = document.getElementsByClassName("CodeMirror-dialog")[0]; + if (prevDialog) { + clearSearch(cm); + prevDialog.parentNode.removeChild(prevDialog); + cm.focus(); + } + if (cm.getOption("readOnly")) return; + var query = cm.getSelection() || getSearchState(cm).lastQuery; + var dialogText = all ? "Replace all:" : "Replace:" + dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) { + if (!query) return; + query = parseQuery(query); + dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) { + text = parseString(text) + if (all) { + replaceAll(cm, query, text) + } else { + clearSearch(cm); + var cursor = getSearchCursor(cm, query, cm.getCursor("from")); + var advance = function() { + var start = cursor.from(), match; + if (!(match = cursor.findNext())) { + cursor = getSearchCursor(cm, query); + if (!(match = cursor.findNext()) || + (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return; + } + cm.setSelection(cursor.from(), cursor.to()); + cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 60); + confirmDialog(cm, doReplaceConfirm, "Replace?", + [function() {doReplace(match);}, advance, + function() {replaceAll(cm, query, text)}]); + }; + var doReplace = function(match) { + cursor.replace(typeof query == "string" ? text : + text.replace(/\$(\d)/g, function(_, i) {return match[i];})); + advance(); + }; + advance(); + } + }); + }); + } + + CodeMirror.commands.find = function(cm) {doSearch(cm);}; + CodeMirror.commands.findPersistent = function(cm) { doSearch(cm, false, true, false, true);}; + CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);}; + CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);}; + CodeMirror.commands.findNext = doSearch; + CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);}; + CodeMirror.commands.clearSearch = clearSearch; + // CodeMirror.commands.replace = replace; + // CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);}; +}; diff --git a/client/utils/metaKey.js b/client/utils/metaKey.js new file mode 100644 index 00000000..e88052e1 --- /dev/null +++ b/client/utils/metaKey.js @@ -0,0 +1,16 @@ +const metaKey = (() => { + if (navigator != null && navigator.platform != null) { + return /^MAC/i.test(navigator.platform) ? + 'Cmd' : + 'Ctrl'; + } + + return 'Ctrl'; +})(); + +const metaKeyName = metaKey === 'Cmd' ? 'Command' : 'Control'; + +export { + metaKey, + metaKeyName, +}; From 1ae37ebaaa534b2671387d0f7977d16bbc9a00ee Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Tue, 13 Jun 2017 16:47:36 -0400 Subject: [PATCH 06/24] fix lingering linting errors --- server/controllers/aws.controller.js | 4 ++-- server/controllers/file.controller.js | 3 ++- server/controllers/project.controller.js | 4 +--- server/examples.js | 6 ++++-- server/utils/previewGeneration.js | 2 +- static/p5-interceptor | 2 +- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/server/controllers/aws.controller.js b/server/controllers/aws.controller.js index eda6b4c3..a22be355 100644 --- a/server/controllers/aws.controller.js +++ b/server/controllers/aws.controller.js @@ -28,12 +28,12 @@ export function getObjectKey(url) { if (urlArray.length === 6) { const key = urlArray.pop(); const userId = urlArray.pop(); - objectKey = `${userId}/${key}` + objectKey = `${userId}/${key}`; } else { const key = urlArray.pop(); objectKey = key; } - return objectKey; + return objectKey; } export function deleteObjectsFromS3(keyList, callback) { diff --git a/server/controllers/file.controller.js b/server/controllers/file.controller.js index a6bc7d51..06f3ff68 100644 --- a/server/controllers/file.controller.js +++ b/server/controllers/file.controller.js @@ -48,7 +48,8 @@ function deleteMany(files, ids) { each(ids, (id, cb) => { if (files.id(id).url) { - if (!process.env.S3_DATE || (process.env.S3_DATE && moment(process.env.S3_DATE) < moment(files.id(id).createdAt))) { + if (!process.env.S3_DATE + || (process.env.S3_DATE && moment(process.env.S3_DATE) < moment(files.id(id).createdAt))) { const objectKey = getObjectKey(files.id(id).url); objectKeys.push(objectKey); } diff --git a/server/controllers/project.controller.js b/server/controllers/project.controller.js index 84b2afa1..cdfb0ac1 100644 --- a/server/controllers/project.controller.js +++ b/server/controllers/project.controller.js @@ -100,9 +100,7 @@ function deleteFilesFromS3(files) { } return false; }) - .map((file) => { - return getObjectKey(file.url); - }) + .map(file => getObjectKey(file.url)) ); } diff --git a/server/examples.js b/server/examples.js index bb23c333..341fa409 100644 --- a/server/examples.js +++ b/server/examples.js @@ -112,7 +112,8 @@ function getSketchContent(projectsInAllCategories) { if (noNumberprojectName === 'Instance Mode : Instance Container ') { for (let i = 0; i < 4; i += 1) { const splitedRes = `${res.split('*/')[1].split('')[i]}\n`; - project.sketchContent = splitedRes.replace('p5.js', 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/p5.min.js'); + project.sketchContent = splitedRes.replace('p5.js', + 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/p5.min.js'); } } else { project.sketchContent = res; @@ -228,7 +229,8 @@ function createProjectsInP5user(projectsInAllCategories) { }); } - const assetsInProject = project.sketchContent.match(/assets\/[\w-]+\.[\w]*/g) || project.sketchContent.match(/assets\/[\w-]*/g) || []; + const assetsInProject = project.sketchContent.match(/assets\/[\w-]+\.[\w]*/g) + || project.sketchContent.match(/assets\/[\w-]*/g) || []; assetsInProject.forEach((assetNamePath, i) => { let assetName = assetNamePath.split('assets/')[1]; diff --git a/server/utils/previewGeneration.js b/server/utils/previewGeneration.js index 5b44d41a..983f1f9c 100644 --- a/server/utils/previewGeneration.js +++ b/server/utils/previewGeneration.js @@ -1,5 +1,5 @@ import { resolvePathToFile } from '../utils/filePath'; - +// eslint-disable-next-line max-len const MEDIA_FILE_REGEX_NO_QUOTES = /^(?!(http:\/\/|https:\/\/)).*\.(png|jpg|jpeg|gif|bmp|mp3|wav|aiff|ogg|json|txt|csv|svg|obj|mp4|ogg|webm|mov|otf|ttf|m4a)$/i; const STRING_REGEX = /(['"])((\\\1|.)*?)\1/gm; const EXTERNAL_LINK_REGEX = /^(http:\/\/|https:\/\/)/; diff --git a/static/p5-interceptor b/static/p5-interceptor index 5b924f34..0958be54 160000 --- a/static/p5-interceptor +++ b/static/p5-interceptor @@ -1 +1 @@ -Subproject commit 5b924f3460886b82d72d0ab0d709f323f8b9a588 +Subproject commit 0958be54482722821159cd3e07777988ee349f37 From 7403b2b2d6f3478e8eacf6e5f886c0d9f04a8e55 Mon Sep 17 00:00:00 2001 From: Zach Rispoli Date: Sun, 18 Jun 2017 17:11:23 -0400 Subject: [PATCH 07/24] Current sketch will stop playing if a new example is opened (issue #357) (#365) * Current sketch will stop playing if a new example is opened (#357) * stopSketch dispatches on route change * Remove extra stopSketcch calls --- client/components/Nav.jsx | 6 +++--- client/routes.jsx | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index 6ef4fb35..a767539b 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -83,7 +83,6 @@ class Nav extends React.PureComponent {

    Open @@ -94,7 +93,9 @@ class Nav extends React.PureComponent { })()}

  • - + Examples

    @@ -187,7 +188,6 @@ Nav.propTypes = { }) }), logoutUser: PropTypes.func.isRequired, - stopSketch: PropTypes.func.isRequired, showShareModal: PropTypes.func.isRequired, showErrorModal: PropTypes.func.isRequired, unsavedChanges: PropTypes.bool.isRequired, diff --git a/client/routes.jsx b/client/routes.jsx index b4bdf6a8..082bc68c 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -11,11 +11,16 @@ import NewPasswordView from './modules/User/pages/NewPasswordView'; import AccountView from './modules/User/pages/AccountView'; // import SketchListView from './modules/Sketch/pages/SketchListView'; import { getUser } from './modules/User/actions'; +import { stopSketch } from './modules/IDE/actions/ide'; const checkAuth = (store) => { store.dispatch(getUser()); }; +const onRouteChange = (store) => { + store.dispatch(stopSketch()); +}; + const routes = (store) => { const sourceProtocol = findSourceProtocol(store.getState()); @@ -28,7 +33,7 @@ const routes = (store) => { }); return ( - + { onRouteChange(store); }}> From 1dc0c22cb7769adf37ad486afd33768c679695e5 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Mon, 26 Jun 2017 18:48:28 +0200 Subject: [PATCH 08/24] Email verification (#369) * Re-introduce Email Verification code Revert "Revert "Email verification"" This reverts commit d154d8bff259350523a0f139e844db96c43d2ee1. * Uses MJML to generate Reset Password email * Sends Password Reset and Email Confirmation emails using MJML template * Sends verified status along with user data * API endpoint for resending email verification confirmation * Displays verification status on Account page and allows resending * Send back error string * Passes email address through to sign/verify helper * Uses enum-style object to set verified state * Sends minimal info when user verifies since it can be done without login * Provides /verify UI and sends confirmation token to API * Better name for JWT secret token env var * Adds mail config variables to Readme * Encrypts email address in JWT The JWT sent as the token in the Confirm Password URL can be unencoded by anyone, although it's signature can only be verified by us. To ensure that no passwords are leaked, we encrypt the email address before creating the token. * Removes unused mail templates * Resets verified flag when email is changed and sends another email * Moves email confirmation functions next to each other * Extracts random token generator to helper * Moves email confirmation actions into Redux - updates the AccountForm label with a message to check inbox - show status when verifying email token * Uses generated token stored in DB for email confirmation * Sets email confirmation status to verified if logging in from Github * Sends email using new method on account creation * Fixes linting errors * Removes replyTo config --- README.md | 4 + client/constants.js | 5 + client/modules/User/actions.js | 35 ++++ .../modules/User/components/AccountForm.jsx | 35 +++- client/modules/User/pages/AccountView.jsx | 6 +- .../User/pages/EmailVerificationView.jsx | 110 +++++++++++ client/modules/User/reducers.js | 8 + client/routes.jsx | 2 + client/styles/components/_forms.scss | 9 + package.json | 5 + server/config/passport.js | 2 + server/controllers/session.controller.js | 2 + server/controllers/user.controller.js | 180 ++++++++++++++---- server/models/user.js | 11 ++ server/routes/server.routes.js | 4 + server/routes/user.routes.js | 4 + server/utils/mail.js | 48 +++++ server/utils/renderMjml.js | 12 ++ server/views/mail.js | 56 ++++++ server/views/mailLayout.js | 59 ++++++ static/images/p5js-square-logo.png | Bin 0 -> 5895 bytes 21 files changed, 554 insertions(+), 43 deletions(-) create mode 100644 client/modules/User/pages/EmailVerificationView.jsx create mode 100644 server/utils/mail.js create mode 100644 server/utils/renderMjml.js create mode 100644 server/views/mail.js create mode 100644 server/views/mailLayout.js create mode 100644 static/images/p5js-square-logo.png diff --git a/README.md b/README.md index 4a3b76fd..6c69e2c7 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,10 @@ The automatic redirection to HTTPS is turned off by default in development. If y S3_BUCKET= GITHUB_ID= GITHUB_SECRET= + EMAIL_SENDER= + MAILGUN_KEY= + MAILGUN_DOMAIN= + EMAIL_VERIFY_SECRET_TOKEN=whatever_you_want_this_to_be_it_only_matters_for_production ``` For production, you will need to have real Github and Amazon credentions. Refer to [this gist](https://gist.github.com/catarak/70c9301f0fd1ac2d6b58de03f61997e3) for creating an S3 bucket for testing. diff --git a/client/constants.js b/client/constants.js index 8f1a5573..3d4d1e8b 100644 --- a/client/constants.js +++ b/client/constants.js @@ -102,6 +102,11 @@ export const RESET_PASSWORD_INITIATE = 'RESET_PASSWORD_INITIATE'; export const RESET_PASSWORD_RESET = 'RESET_PASSWORD_RESET'; export const INVALID_RESET_PASSWORD_TOKEN = 'INVALID_RESET_PASSWORD_TOKEN'; +export const EMAIL_VERIFICATION_INITIATE = 'EMAIL_VERIFICATION_INITIATE'; +export const EMAIL_VERIFICATION_VERIFY = 'EMAIL_VERIFICATION_VERIFY'; +export const EMAIL_VERIFICATION_VERIFIED = 'EMAIL_VERIFICATION_VERIFIED'; +export const EMAIL_VERIFICATION_INVALID = 'EMAIL_VERIFICATION_INVALID'; + // eventually, handle errors more specifically and better export const ERROR = 'ERROR'; diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index 2bcb7d1c..3150da4b 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -130,6 +130,41 @@ export function initiateResetPassword(formValues) { }; } +export function initiateVerification() { + return (dispatch) => { + dispatch({ + type: ActionTypes.EMAIL_VERIFICATION_INITIATE + }); + axios.post(`${ROOT_URL}/verify/send`, {}, { withCredentials: true }) + .then(() => { + // do nothing + }) + .catch(response => dispatch({ + type: ActionTypes.ERROR, + message: response.data + })); + }; +} + +export function verifyEmailConfirmation(token) { + return (dispatch) => { + dispatch({ + type: ActionTypes.EMAIL_VERIFICATION_VERIFY, + state: 'checking', + }); + return axios.get(`${ROOT_URL}/verify?t=${token}`, {}, { withCredentials: true }) + .then(response => dispatch({ + type: ActionTypes.EMAIL_VERIFICATION_VERIFIED, + message: response.data, + })) + .catch(response => dispatch({ + type: ActionTypes.EMAIL_VERIFICATION_INVALID, + message: response.data + })); + }; +} + + export function resetPasswordReset() { return { type: ActionTypes.RESET_PASSWORD_RESET diff --git a/client/modules/User/components/AccountForm.jsx b/client/modules/User/components/AccountForm.jsx index f8b4296e..e504bc9b 100644 --- a/client/modules/User/components/AccountForm.jsx +++ b/client/modules/User/components/AccountForm.jsx @@ -4,11 +4,19 @@ import { domOnlyProps } from '../../../utils/reduxFormUtils'; function AccountForm(props) { const { fields: { username, email, currentPassword, newPassword }, + user, handleSubmit, + initiateVerification, submitting, invalid, pristine } = props; + + const handleInitiateVerification = (evt) => { + evt.preventDefault(); + initiateVerification(); + }; + return (

    @@ -22,6 +30,26 @@ function AccountForm(props) { /> {email.touched && email.error && {email.error}}

    + { + user.verified !== 'verified' && + ( +

    + Unconfirmed. + { + user.emailVerificationInitiate === true ? + ( + Confirmation sent, check your email. + ) : + ( + + ) + } +

    + ) + }

    get(this.props, 'location.query.t', null); + + closeLoginPage() { + browserHistory.push(this.props.previousPath); + } + + gotoHomePage() { + browserHistory.push('/'); + } + + render() { + let status = null; + const { + emailVerificationTokenState, + } = this.props; + + if (this.verificationToken() == null) { + status = ( +

    That link is invalid

    + ); + } else if (emailVerificationTokenState === 'checking') { + status = ( +

    Validating token, please wait...

    + ); + } else if (emailVerificationTokenState === 'verified') { + status = ( +

    All done, your email address has been verified.

    + ); + } else if (emailVerificationTokenState === 'invalid') { + status = ( +

    Something went wrong.

    + ); + } + + return ( +
    +
    + + +
    +
    +

    Verify your email

    + {status} +
    +
    + ); + } +} + +function mapStateToProps(state) { + return { + emailVerificationTokenState: state.user.emailVerificationTokenState, + previousPath: state.ide.previousPath + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators({ + verifyEmailConfirmation, + }, dispatch); +} + + +EmailVerificationView.propTypes = { + previousPath: PropTypes.string.isRequired, + emailVerificationTokenState: PropTypes.oneOf([ + 'checking', 'verified', 'invalid' + ]), + verifyEmailConfirmation: PropTypes.func.isRequired, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(EmailVerificationView); diff --git a/client/modules/User/reducers.js b/client/modules/User/reducers.js index 5554a11f..6989bfc0 100644 --- a/client/modules/User/reducers.js +++ b/client/modules/User/reducers.js @@ -19,6 +19,14 @@ const user = (state = { authenticated: false }, action) => { return Object.assign({}, state, { resetPasswordInitiate: false }); case ActionTypes.INVALID_RESET_PASSWORD_TOKEN: return Object.assign({}, state, { resetPasswordInvalid: true }); + case ActionTypes.EMAIL_VERIFICATION_INITIATE: + return Object.assign({}, state, { emailVerificationInitiate: true }); + case ActionTypes.EMAIL_VERIFICATION_VERIFY: + return Object.assign({}, state, { emailVerificationTokenState: 'checking' }); + case ActionTypes.EMAIL_VERIFICATION_VERIFIED: + return Object.assign({}, state, { emailVerificationTokenState: 'verified' }); + case ActionTypes.EMAIL_VERIFICATION_INVALID: + return Object.assign({}, state, { emailVerificationTokenState: 'invalid' }); case ActionTypes.SETTINGS_UPDATED: return { ...state, ...action.user }; default: diff --git a/client/routes.jsx b/client/routes.jsx index 082bc68c..f5f7ce6b 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -7,6 +7,7 @@ import FullView from './modules/IDE/pages/FullView'; import LoginView from './modules/User/pages/LoginView'; import SignupView from './modules/User/pages/SignupView'; import ResetPasswordView from './modules/User/pages/ResetPasswordView'; +import EmailVerificationView from './modules/User/pages/EmailVerificationView'; import NewPasswordView from './modules/User/pages/NewPasswordView'; import AccountView from './modules/User/pages/AccountView'; // import SketchListView from './modules/Sketch/pages/SketchListView'; @@ -38,6 +39,7 @@ const routes = (store) => { + done(null, existingEmailUser)); } else { const user = new User(); @@ -92,6 +93,7 @@ passport.use(new GitHubStrategy({ user.username = profile.username; user.tokens.push({ kind: 'github', accessToken }); user.name = profile.displayName; + user.verified = User.EmailConfirmation.Verified; user.save(saveErr => done(null, user)); } }); diff --git a/server/controllers/session.controller.js b/server/controllers/session.controller.js index dd35a07f..211b8a46 100644 --- a/server/controllers/session.controller.js +++ b/server/controllers/session.controller.js @@ -13,6 +13,7 @@ export function createSession(req, res, next) { email: req.user.email, username: req.user.username, preferences: req.user.preferences, + verified: req.user.verified, id: req.user._id }); }); @@ -25,6 +26,7 @@ export function getSession(req, res) { email: req.user.email, username: req.user.username, preferences: req.user.preferences, + verified: req.user.verified, id: req.user._id }); } diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index 8785d3e1..fb134f58 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -1,17 +1,34 @@ import crypto from 'crypto'; import async from 'async'; -import nodemailer from 'nodemailer'; -import mg from 'nodemailer-mailgun-transport'; + import User from '../models/user'; +import mail from '../utils/mail'; +import { + renderEmailConfirmation, + renderResetPassword, +} from '../views/mail'; + +const random = (done) => { + crypto.randomBytes(20, (err, buf) => { + const token = buf.toString('hex'); + done(err, token); + }); +}; + +const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours export function createUser(req, res, next) { - const user = new User({ - username: req.body.username, - email: req.body.email, - password: req.body.password - }); + random((tokenError, token) => { + const user = new User({ + username: req.body.username, + email: req.body.email, + password: req.body.password, + verified: User.EmailConfirmation.Sent, + verifiedToken: token, + verifiedTokenExpires: EMAIL_VERIFY_TOKEN_EXPIRY_TIME, + }); - User.findOne({ email: req.body.email }, + User.findOne({ email: req.body.email }, (err, existingUser) => { if (err) { res.status(404).send({ error: err }); @@ -32,15 +49,28 @@ export function createUser(req, res, next) { next(loginErr); return; } - res.json({ - email: req.user.email, - username: req.user.username, - preferences: req.user.preferences, - id: req.user._id + + const mailOptions = renderEmailConfirmation({ + body: { + domain: `http://${req.headers.host}`, + link: `http://${req.headers.host}/verify?t=${token}` + }, + to: req.user.email, + }); + + mail.send(mailOptions, (mailErr, result) => { // eslint-disable-line no-unused-vars + res.json({ + email: req.user.email, + username: req.user.username, + preferences: req.user.preferences, + verified: req.user.verified, + id: req.user._id + }); }); }); }); }); + }); } export function duplicateUserCheck(req, res) { @@ -90,12 +120,7 @@ export function updatePreferences(req, res) { export function resetPasswordInitiate(req, res) { async.waterfall([ - (done) => { - crypto.randomBytes(20, (err, buf) => { - const token = buf.toString('hex'); - done(err, token); - }); - }, + random, (token, done) => { User.findOne({ email: req.body.email }, (err, user) => { if (!user) { @@ -111,27 +136,15 @@ export function resetPasswordInitiate(req, res) { }); }, (token, user, done) => { - const auth = { - auth: { - api_key: process.env.MAILGUN_KEY, - domain: process.env.MAILGUN_DOMAIN - } - }; - - const transporter = nodemailer.createTransport(mg(auth)); - const message = { + const mailOptions = renderResetPassword({ + body: { + domain: `http://${req.headers.host}`, + link: `http://${req.headers.host}/reset-password/${token}`, + }, to: user.email, - from: 'p5.js Web Editor ', - subject: 'p5.js Web Editor Password Reset', - text: `You are receiving this email because you (or someone else) have requested the reset of the password for your account. - \n\nPlease click on the following link, or paste this into your browser to complete the process: - \n\nhttp://${req.headers.host}/reset-password/${token} - \n\nIf you did not request this, please ignore this email and your password will remain unchanged. - \n\nThanks for using the p5.js Web Editor!\n` - }; - transporter.sendMail(message, (error) => { - done(error); }); + + mail.send(mailOptions, done); } ], (err) => { if (err) { @@ -153,6 +166,75 @@ export function validateResetPasswordToken(req, res) { }); } +export function emailVerificationInitiate(req, res) { + async.waterfall([ + random, + (token, done) => { + User.findById(req.user.id, (err, user) => { + if (err) { + res.status(500).json({ error: err }); + return; + } + if (!user) { + res.status(404).json({ error: 'Document not found' }); + return; + } + + if (user.verified === User.EmailConfirmation.Verified) { + res.status(409).json({ error: 'Email already verified' }); + return; + } + + const mailOptions = renderEmailConfirmation({ + body: { + domain: `http://${req.headers.host}`, + link: `http://${req.headers.host}/verify?t=${token}` + }, + to: user.email, + }); + + mail.send(mailOptions, (mailErr, result) => { // eslint-disable-line no-unused-vars + if (mailErr != null) { + res.status(500).send({ error: 'Error sending mail' }); + } else { + user.verified = User.EmailConfirmation.Resent; + user.verifiedToken = token; + user.verifiedTokenExpires = EMAIL_VERIFY_TOKEN_EXPIRY_TIME; // 24 hours + user.save(); + + res.json({ + email: req.user.email, + username: req.user.username, + preferences: req.user.preferences, + verified: user.verified, + id: req.user._id + }); + } + }); + }); + }, + ]); +} + +export function verifyEmail(req, res) { + const token = req.query.t; + + User.findOne({ verifiedToken: token, verifiedTokenExpires: { $gt: Date.now() } }, (err, user) => { + if (!user) { + res.status(401).json({ success: false, message: 'Token is invalid or has expired.' }); + return; + } + + user.verified = User.EmailConfirmation.Verified; + user.verifiedToken = null; + user.verifiedTokenExpires = null; + user.save() + .then((result) => { // eslint-disable-line + res.json({ success: true }); + }); + }); +} + export function updatePassword(req, res) { User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, (err, user) => { if (!user) { @@ -205,7 +287,6 @@ export function updateSettings(req, res) { return; } - user.email = req.body.email; user.username = req.body.username; if (req.body.currentPassword) { @@ -218,6 +299,27 @@ export function updateSettings(req, res) { user.password = req.body.newPassword; saveUser(res, user); }); + } else if (user.email !== req.body.email) { + user.verified = User.EmailConfirmation.Sent; + + user.email = req.body.email; + + random((error, token) => { + user.verifiedToken = token; + user.verifiedTokenExpires = EMAIL_VERIFY_TOKEN_EXPIRY_TIME; + + saveUser(res, user); + + const mailOptions = renderEmailConfirmation({ + body: { + domain: `http://${req.headers.host}`, + link: `http://${req.headers.host}/verify?t=${token}` + }, + to: user.email, + }); + + mail.send(mailOptions); + }); } else { saveUser(res, user); } diff --git a/server/models/user.js b/server/models/user.js index 0f89cb15..5ddaf2f9 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -2,6 +2,12 @@ import mongoose from 'mongoose'; const bcrypt = require('bcrypt-nodejs'); +const EmailConfirmationStates = { + Verified: 'verified', + Sent: 'sent', + Resent: 'resent', +}; + const Schema = mongoose.Schema; const userSchema = new Schema({ @@ -10,6 +16,9 @@ const userSchema = new Schema({ password: { type: String }, resetPasswordToken: String, resetPasswordExpires: Date, + verified: { type: String }, + verifiedToken: String, + verifiedTokenExpires: Date, github: { type: String }, email: { type: String, unique: true }, tokens: Array, @@ -73,4 +82,6 @@ userSchema.statics.findByMailOrName = function findByMailOrName(email) { return this.findOne(query).exec(); }; +userSchema.statics.EmailConfirmation = EmailConfirmationStates; + export default mongoose.model('User', userSchema); diff --git a/server/routes/server.routes.js b/server/routes/server.routes.js index 1b69dbda..7bd944a2 100644 --- a/server/routes/server.routes.js +++ b/server/routes/server.routes.js @@ -40,6 +40,10 @@ router.route('/reset-password/:reset_password_token').get((req, res) => { res.send(renderIndex()); }); +router.route('/verify').get((req, res) => { + res.send(renderIndex()); +}); + router.route('/sketches').get((req, res) => { res.send(renderIndex()); }); diff --git a/server/routes/user.routes.js b/server/routes/user.routes.js index b3468cd8..682ac698 100644 --- a/server/routes/user.routes.js +++ b/server/routes/user.routes.js @@ -17,4 +17,8 @@ router.route('/reset-password/:token').post(UserController.updatePassword); router.route('/account').put(UserController.updateSettings); +router.route('/verify/send').post(UserController.emailVerificationInitiate); + +router.route('/verify').get(UserController.verifyEmail); + export default router; diff --git a/server/utils/mail.js b/server/utils/mail.js new file mode 100644 index 00000000..42c9b317 --- /dev/null +++ b/server/utils/mail.js @@ -0,0 +1,48 @@ +/** + * Mail service wrapping around mailgun + */ + +import nodemailer from 'nodemailer'; +import mg from 'nodemailer-mailgun-transport'; + +const auth = { + api_key: process.env.MAILGUN_KEY, + domain: process.env.MAILGUN_DOMAIN, +}; + +class Mail { + constructor() { + this.client = nodemailer.createTransport(mg({ auth })); + this.sendOptions = { + from: process.env.EMAIL_SENDER, + }; + } + + sendMail(mailOptions) { + return new Promise((resolve, reject) => { + this.client.sendMail(mailOptions, (err, info) => { + resolve(err, info); + }); + }); + } + + dispatchMail(data, callback) { + const mailOptions = { + to: data.to, + subject: data.subject, + from: this.sendOptions.from, + html: data.html, + }; + + return this.sendMail(mailOptions) + .then((err, res) => { + callback(err, res); + }); + } + + send(data, callback) { + return this.dispatchMail(data, callback); + } +} + +export default new Mail(); diff --git a/server/utils/renderMjml.js b/server/utils/renderMjml.js new file mode 100644 index 00000000..1a9cf8f4 --- /dev/null +++ b/server/utils/renderMjml.js @@ -0,0 +1,12 @@ +import { mjml2html } from 'mjml'; + +export default (template) => { + try { + const output = mjml2html(template); + return output.html; + } catch (e) { + // fall through to null + } + + return null; +}; diff --git a/server/views/mail.js b/server/views/mail.js new file mode 100644 index 00000000..949aeb4b --- /dev/null +++ b/server/views/mail.js @@ -0,0 +1,56 @@ +import renderMjml from '../utils/renderMjml'; +import mailLayout from './mailLayout'; + +export const renderResetPassword = (data) => { + const subject = 'p5.js Web Editor Password Reset'; + const templateOptions = { + domain: data.body.domain, + headingText: 'Reset your password', + greetingText: 'Hello,', + messageText: 'We received a request to reset the password for your account. To reset your password, click on the button below:', // eslint-disable-line max-len + link: data.body.link, + buttonText: 'Reset password', + directLinkText: 'Or copy and paste the URL into your browser:', + noteText: 'If you did not request this, please ignore this email and your password will remain unchanged. Thanks for using the p5.js Web Editor!', // eslint-disable-line max-len + }; + + // Return MJML string + const template = mailLayout(templateOptions); + + // Render MJML to HTML string + const html = renderMjml(template); + + // Return options to send mail + return Object.assign( + {}, + data, + { html, subject }, + ); +}; + +export const renderEmailConfirmation = (data) => { + const subject = 'p5.js Email Verification'; + const templateOptions = { + domain: data.body.domain, + headingText: 'Email Verification', + greetingText: 'Hello,', + messageText: 'To verify your email, click on the button below:', + link: data.body.link, + buttonText: 'Verify Email', + directLinkText: 'Or copy and paste the URL into your browser:', + noteText: 'This link is only valid for the next 24 hours. Thanks for using the p5.js Web Editor!', + }; + + // Return MJML string + const template = mailLayout(templateOptions); + + // Render MJML to HTML string + const html = renderMjml(template); + + // Return options to send mail + return Object.assign( + {}, + data, + { html, subject }, + ); +}; diff --git a/server/views/mailLayout.js b/server/views/mailLayout.js new file mode 100644 index 00000000..cd69cab7 --- /dev/null +++ b/server/views/mailLayout.js @@ -0,0 +1,59 @@ +export default ({ + domain, + headingText, + greetingText, + messageText, + link, + buttonText, + directLinkText, + noteText, +}) => ( +` + + + + + + + + + + + + + + ${headingText} + + + + + + + + ${greetingText} + + + ${messageText} + + + ${buttonText} + + + + + + + + ${directLinkText} + + ${link} + + ${noteText} + + + + + + +` +); diff --git a/static/images/p5js-square-logo.png b/static/images/p5js-square-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fd9506b8647c90f9790c11125483c78479e1c9d9 GIT binary patch literal 5895 zcmeI0ldImt(=t{SAwP)0_E`F^mOaM7u}wSm7G zwaA_C;Jei2(W+Fj!zEE4x1QkX@*_ba%wle#-S>1uAEcW3^FWmk&K05DbQV#{&721W zy@P|hO@;1S994XcLNasXbE7imYyJ!VLZfp}%-71#8$Xw5W#l>C7t|%7QAX*3@e(%& z&_Z~0cpAVd7%bJK1$1(=0=QpOU&UmaKd zy)A?P|7HH~O;aERu`IIVsj-69qzx;eXd{|2r?Mr9Msh*WwtS^LGJ%pJbblY8JKf=p zLjF*l-1(%ogvrwmPNbb)m)|F6U+Bn*wEjW5IbjOmi7-ZF?dx7B9L-Yl{8!)QB_^Q- z)Fmu>0UVBSI!{5b!5PTynTlec@{)uT>5$P4G1%dW8&lue1*L~gzHPeo$bQO3u?qsI zl!`;%ijfO}(n|^ZB2Y(wz2AnR{n=hj@C&~r`|mW>p^bZZnz9UJQH|vQn-hkq#{_LU zcHqojc>?^lrU+}xW<>mT?y+Z9U4x$ll^|Mer~_Nc(A=1!zsV>-K4O>bcS0xJtwR91 zEmI`Ls428&;a_;s`iofusESOfxA4SMRA#P_F{ywmc3+nKi&`U&53(gIvMi`=E$C@o z{MwvtJD~()a)okUnfj)mZQ9-2YX_eaQ#fBGdP5Zz-t8ADluQzq0TC)pdRNk!h|00M zbSAGfFL;Z(^uDO`WUhH(qrXnM>-=GY`yYX_t3_wi}YSA{qMxQrtrsg@PP8C>&l%!Xs-eOYBvlxV2?m`>!| zt`xX0*Gq@WB<7;2b)zc+H|=KfZ)`!;Zzw6-OkS-Q%s-ox0q{DrF`fLOBX*XXf94`N zlG)PAk}2oZP=kwqW}_!f8;_P3pN}qhLXqb9GE!A){mc z!Mm5j{mV(l5|nMl6D$Hp!_kTTBts5T`Oz6?kP`;z0sWv*hV5bUGNOK{tYCCptqoih z9>3@_ECNhJ){Sy3Dpo(#UzA9oEtJxA9aFBXalVF!#U%w+cn0C!(?o0!UYxGl;R$PZ z6`oFdl@mMt3$hIB__3*<+I~f-upKS*xqw!p9SoBjP)#kGBKbPKF?0|F-toy@3o7Jx z>pG^-**ZUqZ`dZ+c~yW-yS{2fmHhN1qHN=1^;>0Wg8u15=vbj-BlC1kpFFmOzuLg0 z5<;gVvl=cZQHNLKVLwaoF$d^EO-4Jsk0JfvyF&Qe0~zoLyk?AP>&KG46b?Fv8p``x z8)!!AWah70Pbf@`MJ2BsUyovwy7Y6?8~%0`HQ~M?EMM90NlV}5V_k> z>9)JH`wXRyHwKfnjBQrp_yA9*J(3p_Dl-+2BU#$H7i|Aj_0QzBugQ$8!)eA2=g8|xX z_wL&VT#!&LO?0{b##6`DT0?{jqlM=0iszSyFnX}{3}Kg4EDajX*6XJX!f{_p*@(B} z+qkM_;774C2=OCF7)r{8ZMZZ**pJ>!CjWlY$OJiOz&;n%f&SA&1l6Sf;$TOj6$OCw zC)zbXw6XeNSxXlQt6_~B&7D!!fNaX8!_>i0GDcFI&B!rf%GZ{|7KC|7sAtCq###9Y zQ5HF;Q)O0FTE-xklYF!aE!F$-h}m4~_=`?@@zMA_YuRAA z@RIiq{4I}rdw?dN_Ojm1#1%j1mf?Yv|7i>EDDPc$b2c@9QV(?tJEmitNw&1qJ5`H=#ns}+mK=d}G0 z*gf#s^=|fQ9q;tp;*KA(3fFP;41PY<6r5h}ZplD!hDpw=_VF1BNjLnr>nRH%ZG2ZZ z6$}eoL=R1vhM7=976mVimo!uR1jyL(D3$F$d_c~Z?xYML&jnX3EiHvee(||r7*P&U za!d1EWn$NE;SsH`VAaS-o7svQ|K903D+;Tff&hI&&(4(U(YWyMAtl*68~0IZx{8*# zi$C+HDrPLeFVC%%?gkLAW=$<$6HQ1c;nSApb>X5_`YjvOpYJwF6ML(_V2vIR?e{1p zC;MGKUxRhSq3JIw4HL`{{5)7|y!7olr z_+E7twOM)1yk zDF*on#C@mu(<5g47xY~xvX8RTR4hVbX{5tS_P(6lkS5vAp4UfrEvfy_uS+(F zv`@c0bVWkUQG3xOHVd5-Mk1F&Fr@JJ=LYW40ZsOXY=1~vox@2&q_74BPC3%^uby^;dHys^A{AIf~?i+FC$&H+F%&td!XaqpuE5J*( zNh|D)gQ?E?qzLQh&m(|o|3daThK%;<*!sR< zOEg3GAETV#^nncpvcPbavi;UWn*;Qd-lqbR&sc4kya&;SnVNf>Qoz8N$MgJg1$%sr z$=HcqCGDqqdb^-rO1!SG)1IfSs!Q7jf9Bb0MXu*!fLVvs81xDa2M6Mo2k{p zUw=YXiU4ws+ly>2Ci`h;`PI{&-R(YJG0lo-HLzFmed_7J6b3(D)}i`4f>Za@>qV+8 zM3TT6}6=X)!eiTU&R6`KI*~#tz)K@+Sv~H5GR^s*dRnC z5y!NCv=e8_EqEU~g2c#1OM%-e(Ek!Xn78Zis484{)rIddO4{)Mu71h(bM}CW{0dp= zak1~Z@HJ_^B+49t`n_`!OqXk#7}WXQKd@Z>wadLR`&@}nse-g9fseu)^pfZsW&If) zpE(5Zy4FLV^G9|O7%lEgJutc95njWL-XhOEAS#uXBVSiX(CO5-##a;%(UdlIcty~4qMnZ{pC3JAS)+}nf-^4)ZkI7pbN*3Ig zw;GhvO~%U@bo-xX-II~TV5{qVSt14IsboLk*?t4%Qq0E}v*Q768O$~|><=Hv&4)R^ zaPC;@9tsy?)n*EOcYXqdRJiXz=6~p$5k+kCY0drBZ?&gw`S3&q)FI|6@a&2R^`cqy z9@@C;`w#K?UUHF=Rd?=yjTp?5@jFxHtjAYGSRmTOW;!Y$J4jwyiiAILNvwv$)}hLq z!fKUZI&)+uD~1OQZII5Ofv5Q#aU-~2@-0QZ|0cz_+eAq%+wjsW9LQmrGk;ROA`+5| z8yl|FudXmux63~{wg#&kL1D-SS0bI+>U@(KbmX$3f0l`F&_1VR84T{d$b@>-I-9t! zyOhwa&K#x9(va^wbOG_CSr9bdywhK2ml=CGLyZS$g^w6YiG{uSRVu!k-{u$la0O5| zC@n=q&W~<*?81N=w4_%OPmA4;z(b{A-j*w);ZG)#ZloH1F>^4sJNUYII0$t4r|&353vXihs_<`XT>FP? zCA()`kp7dC=a8~v0IYmC)U9A;XKhNl1MqvUdShu^>w*Z5NLcTh>k&b4!(#+GZ?EO$ zxHpTv?l%@xX>;BPUu9VS9$FVHyNfjpTb6CbdDup>LYXU=6?>TZ6RV{(GfTNNiIuD! zQiO;e*!drpB(00rouyW_o?Av(9vxAT(fXfpO!AbM7AtUV1(6sLp*HpGbEgQb8OGBM z19%(HJAZ4xJW$hLWt~ZR-^drSzuvNIAgn#X5|Q#l`{?=Ks|9J>bbGfi2Op4iR`Ljc zyEV!NEkN&)=P}p?ZKk2iNqM3f-o`ZH^sD%QR&(2$bHT`&A9FduEC-#Vm*joKfYv+Z z(Cw2skG9tvDm(;(p!t6s2ZN`S7OJQPzT6p1d+sn@WtRPOR%ta$q}8E<>i{<{vP^|5 z)##Ubhge?}RgcC#Zf{r04|`_huD$@%+&%NiL;wgS?a$wxuk}fc(bN*Ye`|Ta-47A< zcIC`&e3f^2BR4Df>-@q21=@H6#Z)*29QpF{eNEUJFdOx__JSjq{e>Q%o26yez{X1J z=~IyReYdfNP}EeI3L`N_-5Hcx!P!VF9BOQ}NDj+7Q&3%eXL)~WL1X0eWWk=`zaxrW z3E17KQ~XnmauOruz1(B&FND?B<-rqGvw>W~%hf!Kq)A0o3qH(l+Pt)y^RSI1UIljI zMlwe~NY>Z&f*CH|;q`}g_G~Lme$@8Fm8mSe1YY(^%8U@*Hxcxb5SGE2`i+9f!|R>G z1s(k^9IidSKIpTp{@zk!8vH6ILWHp4MYG^&JiqUJ6iPc#>Ytg%o2+0huPzz;G4KFB zt+%h^J9%o~iGyiE7Z#}j#Uue#X7u$TNX!m=ZA*x$w^6Y*DkYg!NK5s%>(UP(UwZdG zv&k?#x2eu5jOMtC9}&Z!MK#xqEg2fu_}W*U_F&4Md0S?O6MrM#??wI|z3j%u1{0Qe zy}&~&!|_L`Ib{R8bEX64r@5|WdIWz!<?oZ7s1b2=!JM7e?iHPfc&cXdD=wy=-hmEvw4=|+U>&$oj2#=#m(j!+BmpW5I z2wpR=5454Dr`B90(&NaOQlD z66YrxXk+csj94PX3$F0vln<_vVxI+XzJJT4(tIU!)!$RSE;!GZf0geZ3sl#SxL!Y0 zzhk-`7Fr-B#i@2)Xo~5p{Tj%kY@P)g%kwtweQdnBbQ64(-qU2J(evXE9Y{5A5Pe0y8$Acl~cPi<(*Qm z286_@#!I_P+Aw>!?5+=wri8eAnF-HS%xSS5$)$D}Cm#(3Idcid&Mn#mRzV{igz{5% zieG1O6v#axO`7wW3yAN5*fWm2@?Z#y>*S3-hRgF&{ED!A{d2Z! zu_tyX`Kze!PknHSqE3Tg-peTp>eK{drgr9D26C$?r&UqNl zU(#bm2g+E_5x?+h(IV6(Meos*?Yj-gLd}b#928cG`TXD!L1b`@j5;>c)~L#df$qyN zQYLQHdB7(1Ir-v#K|*+e=7v7awASNhc_42%UN?IrnpwD(h53s6>_kyP#y2SE*^`as z0iDZxbMHTmtgc*QV8O59*i1e}a#>=LUA*b#D)AWX)@b(N=bmpuc;~m^^621q6;cID zw0KDT;`rzmYpySM+6BA?DBISke{OEvLQh;z#LzjzExa7!`8Ym$E6t9h$U2s|MVnbh z0wslUwR)*Tlw^Ah1e`4*{-QS|UG!uC%4e&oL9lCWtZ3;1#3AmM> zpY9uH-Ma7l5LEg8ZyLd}4Y?G@*x@U7oO>0y7}iL@l)_E-5z1{ay!X(yvRbldKWTmL xftc`o>fP$?2XBkJFj=>RM%=dL{lCrZd%bj21JNUKv$td$c&wzQSSD{3@*hmv3B~{b literal 0 HcmV?d00001 From 44764050216083d02fc0733dc62f1bf194286003 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Mon, 26 Jun 2017 13:48:24 -0400 Subject: [PATCH 09/24] change all email links to update protocol based on node env --- server/controllers/user.controller.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index fb134f58..ac273d34 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -50,10 +50,11 @@ export function createUser(req, res, next) { return; } + const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'; const mailOptions = renderEmailConfirmation({ body: { - domain: `http://${req.headers.host}`, - link: `http://${req.headers.host}/verify?t=${token}` + domain: `${protocol}://${req.headers.host}`, + link: `${protocol}://${req.headers.host}/verify?t=${token}` }, to: req.user.email, }); @@ -136,10 +137,11 @@ export function resetPasswordInitiate(req, res) { }); }, (token, user, done) => { + const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'; const mailOptions = renderResetPassword({ body: { - domain: `http://${req.headers.host}`, - link: `http://${req.headers.host}/reset-password/${token}`, + domain: `${protocol}://${req.headers.host}`, + link: `${protocol}://${req.headers.host}/reset-password/${token}`, }, to: user.email, }); @@ -185,10 +187,11 @@ export function emailVerificationInitiate(req, res) { return; } + const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'; const mailOptions = renderEmailConfirmation({ body: { - domain: `http://${req.headers.host}`, - link: `http://${req.headers.host}/verify?t=${token}` + domain: `${protocol}://${req.headers.host}`, + link: `${protocol}://${req.headers.host}/verify?t=${token}` }, to: user.email, }); @@ -310,10 +313,11 @@ export function updateSettings(req, res) { saveUser(res, user); + const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'; const mailOptions = renderEmailConfirmation({ body: { - domain: `http://${req.headers.host}`, - link: `http://${req.headers.host}/verify?t=${token}` + domain: `${protocol}://${req.headers.host}`, + link: `${protocol}://${req.headers.host}/verify?t=${token}` }, to: user.email, }); From 6cbc376d6e4c2df94da67392fd7ec0af53dc67a8 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Mon, 26 Jun 2017 19:58:58 +0200 Subject: [PATCH 10/24] CSRF/XSS protection (#374) * /api endpoints only allows requests with application/json Content-Type Otherwise sends 406 Unacceptable * Uses CSRF token The CSRF token is sent as the cookie 'XSRF-TOKEN' on all HTML page requests. This token is picked up automatically by axios and sent to the API with all requests as an 'X-XSRF-TOKEN' header. The middleware runs on all routes and verifies that the token matches what's stored in the session. --- package.json | 1 + server/server.js | 25 ++++++++++++++++++------- server/utils/requestsOfType.js | 15 +++++++++++++++ 3 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 server/utils/requestsOfType.js diff --git a/package.json b/package.json index edbb976a..61aeb26b 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "cookie-parser": "^1.4.1", "cors": "^2.8.1", "csslint": "^0.10.0", + "csurf": "^1.9.0", "decomment": "^0.8.7", "dotenv": "^2.0.0", "dropzone": "^4.3.0", diff --git a/server/server.js b/server/server.js index 79baca8d..b8d19c80 100644 --- a/server/server.js +++ b/server/server.js @@ -7,6 +7,7 @@ import session from 'express-session'; import connectMongo from 'connect-mongo'; import passport from 'passport'; import path from 'path'; +import csurf from 'csurf'; // Webpack Requirements import webpack from 'webpack'; @@ -23,6 +24,7 @@ import files from './routes/file.routes'; import aws from './routes/aws.routes'; import serverRoutes from './routes/server.routes'; import embedRoutes from './routes/embed.routes'; +import { requestsOfTypeJSON } from './utils/requestsOfType'; import { renderIndex } from './views/index'; import { get404Sketch } from './views/404Page'; @@ -73,18 +75,27 @@ app.use(session({ autoReconnect: true }) })); + +// Enables CSRF protection and stores secret in session +app.use(csurf()); +// Middleware to add CSRF token as cookie to some requests +const csrfToken = (req, res, next) => { + res.cookie('XSRF-TOKEN', req.csrfToken()); + next(); +}; + app.use(passport.initialize()); app.use(passport.session()); -app.use('/api', users); -app.use('/api', sessions); -app.use('/api', projects); -app.use('/api', files); -app.use('/api', aws); +app.use('/api', requestsOfTypeJSON(), users); +app.use('/api', requestsOfTypeJSON(), sessions); +app.use('/api', requestsOfTypeJSON(), projects); +app.use('/api', requestsOfTypeJSON(), files); +app.use('/api', requestsOfTypeJSON(), aws); // this is supposed to be TEMPORARY -- until i figure out // isomorphic rendering -app.use('/', serverRoutes); +app.use('/', csrfToken, serverRoutes); -app.use('/', embedRoutes); +app.use('/', csrfToken, embedRoutes); app.get('/auth/github', passport.authenticate('github')); app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/login' }), (req, res) => { res.redirect('/'); diff --git a/server/utils/requestsOfType.js b/server/utils/requestsOfType.js new file mode 100644 index 00000000..cac9bca4 --- /dev/null +++ b/server/utils/requestsOfType.js @@ -0,0 +1,15 @@ +/* + express middleware that sends a 406 Unacceptable + response if an incoming request's Content-Type + header does not match `type` +*/ +const requestsOfType = type => (req, res, next) => { + if (req.get('content-type') != null && !req.is(type)) { + return next({ statusCode: 406 }); // 406 UNACCEPTABLE + } + + return next(); +}; + +export default requestsOfType; +export const requestsOfTypeJSON = () => requestsOfType('application/json'); From 607a27a4a1af03b395d4a6af5ab6e5ab86082814 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Mon, 26 Jun 2017 15:07:51 -0400 Subject: [PATCH 11/24] remove jsonwebtoken as a dependency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 61aeb26b..544f1764 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,6 @@ "js-beautify": "^1.6.4", "jsdom": "^9.8.3", "jshint": "^2.9.4", - "jsonwebtoken": "^7.2.1", "lodash": "^4.16.4", "loop-protect": "git+https://git@github.com/catarak/loop-protect.git", "mjml": "^3.3.2", From 05329903141122e580c06749010be1af30e73088 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Mon, 26 Jun 2017 15:11:42 -0400 Subject: [PATCH 12/24] update p5 version to latest --- client/modules/IDE/reducers/files.js | 6 +++--- server/examples.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/modules/IDE/reducers/files.js b/client/modules/IDE/reducers/files.js index 28e44a9a..0449a277 100644 --- a/client/modules/IDE/reducers/files.js +++ b/client/modules/IDE/reducers/files.js @@ -13,9 +13,9 @@ const defaultHTML = ` - - - + + + diff --git a/server/examples.js b/server/examples.js index 341fa409..1e9955e1 100644 --- a/server/examples.js +++ b/server/examples.js @@ -11,9 +11,9 @@ const defaultHTML = ` - - - + + + From 1359a72f2d70764c3bf97c8910f587e1a6ba298c Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Mon, 26 Jun 2017 15:39:21 -0400 Subject: [PATCH 13/24] fix #372 --- client/modules/IDE/components/PreviewFrame.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/modules/IDE/components/PreviewFrame.jsx b/client/modules/IDE/components/PreviewFrame.jsx index 31bcd90d..c29d2e54 100644 --- a/client/modules/IDE/components/PreviewFrame.jsx +++ b/client/modules/IDE/components/PreviewFrame.jsx @@ -280,7 +280,7 @@ class PreviewFrame extends React.Component { } } }); - newContent = decomment(newContent); + newContent = decomment(newContent, { ignore: /noprotect/g }); newContent = loopProtect(newContent); return newContent; } From 080e9aa8235ba929a7351b115557cbfc5c8961f0 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Mon, 26 Jun 2017 17:46:47 -0400 Subject: [PATCH 14/24] fix #370 by moving icon placeholder to a mixin --- client/styles/abstracts/_mixins.scss | 20 ++++++++++++++++++++ client/styles/abstracts/_placeholders.scss | 20 -------------------- client/styles/components/_about.scss | 4 +--- client/styles/components/_console.scss | 8 ++------ client/styles/components/_editor.scss | 12 +++--------- client/styles/components/_error-modal.scss | 4 +--- client/styles/components/_modal.scss | 8 ++------ client/styles/components/_preferences.scss | 4 +--- client/styles/components/_sidebar.scss | 14 ++++---------- client/styles/components/_sketch-list.scss | 4 +--- 10 files changed, 35 insertions(+), 63 deletions(-) diff --git a/client/styles/abstracts/_mixins.scss b/client/styles/abstracts/_mixins.scss index 54b836ec..1e6f2c52 100644 --- a/client/styles/abstracts/_mixins.scss +++ b/client/styles/abstracts/_mixins.scss @@ -16,4 +16,24 @@ $theme-map: null !global; } } +} + +@mixin icon() { + @include themify() { + color: getThemifyVariable('icon-color'); + & g { + fill: getThemifyVariable('icon-color'); + } + &:hover { + color: getThemifyVariable('icon-hover-color'); + & g { + opacity: 1; + fill: getThemifyVariable('icon-hover-color'); + } + } + } + background-color: transparent; + border: none; + cursor: pointer; + padding: 0; } \ No newline at end of file diff --git a/client/styles/abstracts/_placeholders.scss b/client/styles/abstracts/_placeholders.scss index 8c6ce582..ec837235 100644 --- a/client/styles/abstracts/_placeholders.scss +++ b/client/styles/abstracts/_placeholders.scss @@ -31,26 +31,6 @@ } } -%icon { - @include themify() { - color: getThemifyVariable('icon-color'); - & g { - fill: getThemifyVariable('icon-color'); - } - &:hover { - color: getThemifyVariable('icon-hover-color'); - & g { - opacity: 1; - fill: getThemifyVariable('icon-hover-color'); - } - } - } - background-color: transparent; - border: none; - cursor: pointer; - padding: 0; -} - %icon-toast{ @include themify() { color: $toast-text-color diff --git a/client/styles/components/_about.scss b/client/styles/components/_about.scss index 4cd4ea4e..1ca80ea2 100644 --- a/client/styles/components/_about.scss +++ b/client/styles/components/_about.scss @@ -25,9 +25,7 @@ } .about__exit-button { - @include themify() { - @extend %icon; - } + @include icon(); } .about__logo { diff --git a/client/styles/components/_console.scss b/client/styles/components/_console.scss index 93a55dcd..9ed29fc7 100644 --- a/client/styles/components/_console.scss +++ b/client/styles/components/_console.scss @@ -72,9 +72,7 @@ .preview-console__collapse { padding-top: #{3 / $base-font-size}rem; - @include themify() { - @extend %icon; - } + @include icon(); .preview-console--collapsed & { display: none; } @@ -82,9 +80,7 @@ .preview-console__expand { padding-top: #{3 / $base-font-size}rem; - @include themify() { - @extend %icon; - } + @include icon(); display: none; .preview-console--collapsed & { display: inline-block; diff --git a/client/styles/components/_editor.scss b/client/styles/components/_editor.scss index 6b460d9c..75647316 100644 --- a/client/styles/components/_editor.scss +++ b/client/styles/components/_editor.scss @@ -187,9 +187,7 @@ width: 14px; height: 14px; - @include themify() { - @extend %icon; - } + @include icon(); background-repeat: no-repeat; background-position: center; @@ -231,9 +229,7 @@ margin-left: #{8 / $base-font-size}rem; - @include themify() { - @extend %icon; - } + @include icon(); background: transparent url(../images/exit.svg) no-repeat; } @@ -249,9 +245,7 @@ } .editor__options-button { - @include themify() { - @extend %icon; - } + @include icon(); position: absolute; top: #{10 / $base-font-size}rem; right: #{2 / $base-font-size}rem; diff --git a/client/styles/components/_error-modal.scss b/client/styles/components/_error-modal.scss index 35eb940f..f20f05a5 100644 --- a/client/styles/components/_error-modal.scss +++ b/client/styles/components/_error-modal.scss @@ -12,9 +12,7 @@ } .error-modal__exit-button { - @include themify() { - @extend %icon; - } + @include icon(); } .error-modal__content { diff --git a/client/styles/components/_modal.scss b/client/styles/components/_modal.scss index 0999c89c..470509af 100644 --- a/client/styles/components/_modal.scss +++ b/client/styles/components/_modal.scss @@ -22,9 +22,7 @@ } .modal__exit-button { - @include themify() { - @extend %icon; - } + @include icon(); } .modal__header { @@ -96,9 +94,7 @@ } .keyboard-shortcuts__close { - @include themify() { - @extend %icon; - } + @include icon(); } .keyboard-shortcut-item { diff --git a/client/styles/components/_preferences.scss b/client/styles/components/_preferences.scss index f4938d3f..d45adebb 100644 --- a/client/styles/components/_preferences.scss +++ b/client/styles/components/_preferences.scss @@ -17,9 +17,7 @@ } .preferences__exit-button { - @include themify() { - @extend %icon; - } + @include icon(); padding-top: #{5 / $base-font-size}rem; margin-right: #{-6 / $base-font-size}rem; } diff --git a/client/styles/components/_sidebar.scss b/client/styles/components/_sidebar.scss index 86efd929..dba607e0 100644 --- a/client/styles/components/_sidebar.scss +++ b/client/styles/components/_sidebar.scss @@ -25,9 +25,7 @@ } .sidebar__add { - @include themify() { - @extend %icon; - } + @include icon(); .sidebar--contracted & { display: none; } @@ -124,8 +122,8 @@ } .sidebar__file-item-show-options { + @include icon(); @include themify() { - @extend %icon; padding: #{4 / $base-font-size}rem 0; background-color: map-get($theme-map, 'file-selected-color'); padding-right: #{6 / $base-font-size}rem; @@ -183,9 +181,7 @@ } .sidebar__expand { - @include themify() { - @extend %icon; - } + @include icon(); position: absolute; top: #{7 / $base-font-size}rem; left: #{1 / $base-font-size}rem; @@ -200,9 +196,7 @@ } .sidebar__contract { - @include themify() { - @extend %icon; - } + @include icon(); position: absolute; top: #{7 / $base-font-size}rem; left: #{34 / $base-font-size}rem; diff --git a/client/styles/components/_sketch-list.scss b/client/styles/components/_sketch-list.scss index 5cc15dd8..9775b04c 100644 --- a/client/styles/components/_sketch-list.scss +++ b/client/styles/components/_sketch-list.scss @@ -68,9 +68,7 @@ } .sketch-list__exit-button { - @include themify() { - @extend %icon; - } + @include icon(); margin: #{12 / $base-font-size}rem #{16 / $base-font-size}rem; } From e140702784b4e7a096865a47abe342f24543fd2b Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Tue, 11 Jul 2017 17:37:43 +0200 Subject: [PATCH 15/24] Create Asset List View and refactor overlay code (#356) * start to create asset list * begin refactoring overlay component to remove duplicate code * refactoring of overlays, asset list styles * changes to add size to asset list * fixes to asset list * handle case in which a user hasn't uploaded any assets * fix bug in which asset list only grabbed first asset * remove console.log * update overlay exit styling to use icon mixin --- client/components/Nav.jsx | 5 + client/constants.js | 3 + client/modules/App/components/Overlay.jsx | 67 +++++- client/modules/IDE/actions/assets.js | 30 +++ client/modules/IDE/components/About.jsx | 214 ++++++++---------- client/modules/IDE/components/AssetList.jsx | 71 ++++++ client/modules/IDE/components/ErrorModal.jsx | 38 +--- .../IDE/components/KeyboardShortcutModal.jsx | 173 +++++++------- client/modules/IDE/components/ShareModal.jsx | 85 +++---- client/modules/IDE/components/SketchList.jsx | 109 ++++----- client/modules/IDE/pages/IDEView.jsx | 55 ++++- client/modules/IDE/reducers/assets.js | 12 + client/reducers.js | 4 +- client/routes.jsx | 1 + client/styles/abstracts/_placeholders.scss | 2 +- client/styles/components/_about.scss | 36 +-- client/styles/components/_asset-list.scss | 56 +++++ .../components/_keyboard-shortcuts.scss | 20 ++ client/styles/components/_modal.scss | 63 ------ client/styles/components/_overlay.scss | 23 +- client/styles/components/_share.scss | 24 ++ client/styles/components/_sketch-list.scss | 27 +-- client/styles/components/_uploader.scss | 6 + client/styles/main.scss | 3 + package.json | 1 + server/controllers/aws.controller.js | 44 ++++ server/controllers/project.controller.js | 24 +- server/controllers/user.controller.js | 15 ++ server/routes/aws.routes.js | 1 + server/routes/server.routes.js | 6 + 30 files changed, 716 insertions(+), 502 deletions(-) create mode 100644 client/modules/IDE/actions/assets.js create mode 100644 client/modules/IDE/components/AssetList.jsx create mode 100644 client/modules/IDE/reducers/assets.js create mode 100644 client/styles/components/_asset-list.scss create mode 100644 client/styles/components/_keyboard-shortcuts.scss create mode 100644 client/styles/components/_share.scss diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index a767539b..0a861e97 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -142,6 +142,11 @@ class Nav extends React.PureComponent { My sketches
  • +
  • + + My assets + +
  • My account diff --git a/client/constants.js b/client/constants.js index 3d4d1e8b..81548d52 100644 --- a/client/constants.js +++ b/client/constants.js @@ -1,3 +1,5 @@ +// TODO Organize this file by reducer type, ot break this apart into +// multiple files export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT'; export const TOGGLE_SKETCH = 'TOGGLE_SKETCH'; @@ -124,3 +126,4 @@ export const CLEAR_PERSISTED_STATE = 'CLEAR_PERSISTED_STATE'; export const SHOW_HELP_MODAL = 'SHOW_HELP_MODAL'; export const HIDE_HELP_MODAL = 'HIDE_HELP_MODAL'; +export const SET_ASSETS = 'SET_ASSETS'; diff --git a/client/modules/App/components/Overlay.jsx b/client/modules/App/components/Overlay.jsx index 6331d017..8951df64 100644 --- a/client/modules/App/components/Overlay.jsx +++ b/client/modules/App/components/Overlay.jsx @@ -1,21 +1,70 @@ import React, { PropTypes } from 'react'; +import InlineSVG from 'react-inlinesvg'; +import { browserHistory } from 'react-router'; -function Overlay(props) { - return ( -
    -
    - {props.children} +const exitUrl = require('../../../images/exit.svg'); + +class Overlay extends React.Component { + constructor(props) { + super(props); + this.close = this.close.bind(this); + } + + componentDidMount() { + this.overlay.focus(); + } + + close() { + if (!this.props.closeOverlay) { + browserHistory.push(this.props.previousPath); + } else { + this.props.closeOverlay(); + } + } + + render() { + const { + ariaLabel, + title, + children + } = this.props; + return ( +
    +
    +
    { this.overlay = element; }} + className="overlay__body" + > +
    +

    {title}

    + +
    + {children} +
    +
    -
    - ); + ); + } } Overlay.propTypes = { - children: PropTypes.element + children: PropTypes.element, + closeOverlay: PropTypes.func, + title: PropTypes.string, + ariaLabel: PropTypes.string, + previousPath: PropTypes.string.isRequired }; Overlay.defaultProps = { - children: null + children: null, + title: 'Modal', + closeOverlay: null, + ariaLabel: 'modal' }; export default Overlay; diff --git a/client/modules/IDE/actions/assets.js b/client/modules/IDE/actions/assets.js new file mode 100644 index 00000000..8cceb91e --- /dev/null +++ b/client/modules/IDE/actions/assets.js @@ -0,0 +1,30 @@ +import axios from 'axios'; + +import * as ActionTypes from '../../../constants'; + +const ROOT_URL = process.env.API_URL; + +function setAssets(assets) { + return { + type: ActionTypes.SET_ASSETS, + assets + }; +} + +export function getAssets(username) { + return (dispatch, getState) => { + axios.get(`${ROOT_URL}/S3/${username}/objects`, { withCredentials: true }) + .then((response) => { + dispatch(setAssets(response.data.assets)); + }) + .catch(response => dispatch({ + type: ActionTypes.ERROR + })); + }; +} + +export function deleteAsset(assetKey, userId) { + return { + type: 'PLACEHOLDER' + }; +} diff --git a/client/modules/IDE/components/About.jsx b/client/modules/IDE/components/About.jsx index 7fa2bd39..aa734c7c 100644 --- a/client/modules/IDE/components/About.jsx +++ b/client/modules/IDE/components/About.jsx @@ -1,131 +1,101 @@ -import React, { PropTypes } from 'react'; +import React from 'react'; import InlineSVG from 'react-inlinesvg'; -import { browserHistory } from 'react-router'; -const exitUrl = require('../../../images/exit.svg'); const squareLogoUrl = require('../../../images/p5js-square-logo.svg'); const playUrl = require('../../../images/play.svg'); const asteriskUrl = require('../../../images/p5-asterisk.svg'); -class About extends React.Component { - constructor(props) { - super(props); - this.closeAboutModal = this.closeAboutModal.bind(this); - } - - componentDidMount() { - this.aboutSection.focus(); - } - - closeAboutModal() { - browserHistory.push(this.props.previousPath); - } - - render() { - return ( -
    { this.aboutSection = element; }} tabIndex="0"> -
    -

    Welcome

    - -
    -
    - -
    -

    New to p5.js?

    -

    - - - Examples -

    -

    - - - Tutorials -

    -
    -
    -

    Resources

    -

    - - - Libraries -

    -

    - - - Reference -

    -

    - - - Forum -

    -
    -
    -
    -

    - Contribute -

    -

    - Report a bug -

    -

    - Twitter -

    - -
    -
    - ); - } +function About(props) { + return ( +
    + +
    +

    New to p5.js?

    +

    + + + Examples +

    +

    + + + Tutorials +

    +
    +
    +

    Resources

    +

    + + + Libraries +

    +

    + + + Reference +

    +

    + + + Forum +

    +
    +
    +

    + Contribute +

    +

    + Report a bug +

    +

    + Twitter +

    +
    +
    + ); } -About.propTypes = { - previousPath: PropTypes.string.isRequired -}; - export default About; diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx new file mode 100644 index 00000000..e78de28c --- /dev/null +++ b/client/modules/IDE/components/AssetList.jsx @@ -0,0 +1,71 @@ +import React, { PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { Link } from 'react-router'; +import prettyBytes from 'pretty-bytes'; + +import * as AssetActions from '../actions/assets'; + + +class AssetList extends React.Component { + constructor(props) { + super(props); + this.props.getAssets(this.props.username); + } + + render() { + return ( +
    + {this.props.assets.length === 0 && +

    No uploaded assets.

    + } + {this.props.assets.length > 0 && + + + + + + + + + + + {this.props.assets.map(asset => + + + + + + + )} + +
    NameSizeViewSketch
    {asset.name}{prettyBytes(asset.size)}View{asset.sketchName}
    } +
    + ); + } +} + +AssetList.propTypes = { + username: PropTypes.string.isRequired, + assets: PropTypes.arrayOf(PropTypes.shape({ + key: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + sketchName: PropTypes.string.isRequired, + sketchId: PropTypes.string.isRequired + })).isRequired, + getAssets: PropTypes.func.isRequired, +}; + +function mapStateToProps(state) { + return { + user: state.user, + assets: state.assets + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators(Object.assign({}, AssetActions), dispatch); +} + +export default connect(mapStateToProps, mapDispatchToProps)(AssetList); diff --git a/client/modules/IDE/components/ErrorModal.jsx b/client/modules/IDE/components/ErrorModal.jsx index bf62721a..b7fd0851 100644 --- a/client/modules/IDE/components/ErrorModal.jsx +++ b/client/modules/IDE/components/ErrorModal.jsx @@ -1,15 +1,7 @@ import React, { PropTypes } from 'react'; -import InlineSVG from 'react-inlinesvg'; import { Link } from 'react-router'; -const exitUrl = require('../../../images/exit.svg'); - class ErrorModal extends React.Component { - componentDidMount() { - this.errorModal.focus(); - } - - forceAuthentication() { return (

    @@ -40,25 +32,17 @@ class ErrorModal extends React.Component { render() { return ( -

    { this.errorModal = element; }} tabIndex="0"> -
    -

    Error

    - -
    -
    - {(() => { // 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(); - } - })()} -
    -
    +
    + {(() => { // 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(); + } + })()} +
    ); } } diff --git a/client/modules/IDE/components/KeyboardShortcutModal.jsx b/client/modules/IDE/components/KeyboardShortcutModal.jsx index 4101a6f4..0eb399ca 100644 --- a/client/modules/IDE/components/KeyboardShortcutModal.jsx +++ b/client/modules/IDE/components/KeyboardShortcutModal.jsx @@ -1,105 +1,84 @@ -import React, { PropTypes } from 'react'; -import InlineSVG from 'react-inlinesvg'; +import React from 'react'; import { metaKeyName, } from '../../../utils/metaKey'; -const exitUrl = require('../../../images/exit.svg'); - -class KeyboardShortcutModal extends React.Component { - componentDidMount() { - this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1; - } - - render() { - return ( -
    -
    -

    Keyboard Shortcuts

    - -
    -
      -
    • - Shift + Tab - Tidy -
    • -
    • - - {metaKeyName} + S - - Save -
    • -
    • - - {metaKeyName} + F - - Find Text -
    • -
    • - - {metaKeyName} + G - - Find Next Text Match -
    • -
    • - - {metaKeyName} + Shift + G - - Find Previous Text Match -
    • -
    • - - {metaKeyName} + [ - - Indent Code Left -
    • -
    • - - {metaKeyName} + ] - - Indent Code Right -
    • -
    • - - {metaKeyName} + / - - Comment Line -
    • -
    • - - {metaKeyName} + Enter - - Start Sketch -
    • -
    • - - {metaKeyName} + Shift + Enter - - Stop Sketch -
    • -
    • - - {metaKeyName} + Shift + 1 - - Toggle Text-based Canvas -
    • -
    • - - {metaKeyName} + Shift + 2 - - Turn Off Text-based Canvas -
    • -
    -
    - ); - } +function KeyboardShortcutModal() { + return ( +
      +
    • + Shift + Tab + Tidy +
    • +
    • + + {metaKeyName} + S + + Save +
    • +
    • + + {metaKeyName} + F + + Find Text +
    • +
    • + + {metaKeyName} + G + + Find Next Text Match +
    • +
    • + + {metaKeyName} + Shift + G + + Find Previous Text Match +
    • +
    • + + {metaKeyName} + [ + + Indent Code Left +
    • +
    • + + {metaKeyName} + ] + + Indent Code Right +
    • +
    • + + {metaKeyName} + / + + Comment Line +
    • +
    • + + {metaKeyName} + Enter + + Start Sketch +
    • +
    • + + {metaKeyName} + Shift + Enter + + Stop Sketch +
    • +
    • + + {metaKeyName} + Shift + 1 + + Toggle Text-based Canvas +
    • +
    • + + {metaKeyName} + Shift + 2 + + Turn Off Text-based Canvas +
    • +
    + ); } -KeyboardShortcutModal.propTypes = { - closeModal: PropTypes.func.isRequired -}; - export default KeyboardShortcutModal; diff --git a/client/modules/IDE/components/ShareModal.jsx b/client/modules/IDE/components/ShareModal.jsx index ad8c030e..3b398e1b 100644 --- a/client/modules/IDE/components/ShareModal.jsx +++ b/client/modules/IDE/components/ShareModal.jsx @@ -1,57 +1,46 @@ import React, { PropTypes } from 'react'; -import InlineSVG from 'react-inlinesvg'; -const exitUrl = require('../../../images/exit.svg'); - -class ShareModal extends React.Component { - componentDidMount() { - this.shareModal.focus(); - } - render() { - const hostname = window.location.origin; - return ( -
    { this.shareModal = element; }} tabIndex="0"> -
    -

    Share Sketch

    - -
    -
    - - `} - /> -
    -
    - - -
    -
    - - -
    -
    - ); - } +function ShareModal(props) { + const { + projectId, + ownerUsername + } = props; + const hostname = window.location.origin; + return ( +
    +
    + + `} + /> +
    +
    + + +
    +
    + + +
    +
    + ); } ShareModal.propTypes = { projectId: PropTypes.string.isRequired, - closeShareModal: PropTypes.func.isRequired, ownerUsername: PropTypes.string.isRequired }; diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index a60a1add..7094d72b 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -8,80 +8,62 @@ import * as SketchActions from '../actions/projects'; import * as ProjectActions from '../actions/project'; import * as ToastActions from '../actions/toast'; -const exitUrl = require('../../../images/exit.svg'); const trashCan = require('../../../images/trash-can.svg'); class SketchList extends React.Component { constructor(props) { super(props); - this.closeSketchList = this.closeSketchList.bind(this); this.props.getProjects(this.props.username); } - componentDidMount() { - document.getElementById('sketchlist').focus(); - } - - closeSketchList() { - browserHistory.push(this.props.previousPath); - } - render() { const username = this.props.username !== undefined ? this.props.username : this.props.user.username; return ( -
    -
    -

    Open a Sketch

    - -
    -
    - - - - - - - +
    +
    SketchDate createdDate updated
    + + + + + + + + + + {this.props.sketches.map(sketch => + // eslint-disable-next-line + browserHistory.push(`/${username}/sketches/${sketch.id}`)} + > + + + + - - - {this.props.sketches.map(sketch => - // eslint-disable-next-line - browserHistory.push(`/${username}/sketches/${sketch.id}`)} - > - - - - - - )} - -
    SketchDate createdDate updated
    + {(() => { // eslint-disable-line + if (this.props.username === this.props.user.username || this.props.username === undefined) { + return ( + + ); + } + })()} + {sketch.name}{moment(sketch.createdAt).format('MMM D, YYYY h:mm A')}{moment(sketch.updatedAt).format('MMM D, YYYY h:mm A')}
    - {(() => { // eslint-disable-line - if (this.props.username === this.props.user.username || this.props.username === undefined) { - return ( - - ); - } - })()} - {sketch.name}{moment(sketch.createdAt).format('MMM D, YYYY h:mm A')}{moment(sketch.updatedAt).format('MMM D, YYYY h:mm A')}
    -
    -
    + )} + + +
    ); } } @@ -98,8 +80,7 @@ SketchList.propTypes = { updatedAt: PropTypes.string.isRequired })).isRequired, username: PropTypes.string, - deleteProject: PropTypes.func.isRequired, - previousPath: PropTypes.string.isRequired, + deleteProject: PropTypes.func.isRequired }; SketchList.defaultProps = { diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 1968c341..1d503291 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -30,6 +30,7 @@ import * as ConsoleActions from '../actions/console'; import { getHTMLFile } from '../reducers/files'; import Overlay from '../../App/components/Overlay'; import SketchList from '../components/SketchList'; +import AssetList from '../components/AssetList'; import About from '../components/About'; class IDEView extends React.Component { @@ -425,10 +426,30 @@ class IDEView extends React.Component { {(() => { // eslint-disable-line if (this.props.location.pathname.match(/sketches$/)) { return ( - + + + ); + } + })()} + {(() => { // eslint-disable-line + if (this.props.location.pathname.match(/assets$/)) { + return ( + + ); @@ -437,7 +458,11 @@ class IDEView extends React.Component { {(() => { // eslint-disable-line if (this.props.location.pathname === '/about') { return ( - + ); @@ -446,10 +471,13 @@ class IDEView extends React.Component { {(() => { // eslint-disable-line if (this.props.ide.shareModalVisible) { return ( - + @@ -459,10 +487,12 @@ class IDEView extends React.Component { {(() => { // eslint-disable-line if (this.props.ide.keyboardShortcutVisible) { return ( - - + + ); } @@ -470,10 +500,13 @@ class IDEView extends React.Component { {(() => { // eslint-disable-line if (this.props.ide.errorType) { return ( - + ); diff --git a/client/modules/IDE/reducers/assets.js b/client/modules/IDE/reducers/assets.js new file mode 100644 index 00000000..260660f2 --- /dev/null +++ b/client/modules/IDE/reducers/assets.js @@ -0,0 +1,12 @@ +import * as ActionTypes from '../../../constants'; + +const assets = (state = [], action) => { + switch (action.type) { + case ActionTypes.SET_ASSETS: + return action.assets; + default: + return state; + } +}; + +export default assets; diff --git a/client/reducers.js b/client/reducers.js index 4505623a..c508c8ea 100644 --- a/client/reducers.js +++ b/client/reducers.js @@ -9,6 +9,7 @@ import user from './modules/User/reducers'; import sketches from './modules/IDE/reducers/projects'; import toast from './modules/IDE/reducers/toast'; import console from './modules/IDE/reducers/console'; +import assets from './modules/IDE/reducers/assets'; const rootReducer = combineReducers({ form, @@ -20,7 +21,8 @@ const rootReducer = combineReducers({ sketches, editorAccessibility, toast, - console + console, + assets }); export default rootReducer; diff --git a/client/routes.jsx b/client/routes.jsx index f5f7ce6b..556d9970 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -49,6 +49,7 @@ const routes = (store) => { + diff --git a/client/styles/abstracts/_placeholders.scss b/client/styles/abstracts/_placeholders.scss index ec837235..9f3a2038 100644 --- a/client/styles/abstracts/_placeholders.scss +++ b/client/styles/abstracts/_placeholders.scss @@ -186,4 +186,4 @@ color: getThemifyVariable('primary-text-color'); } } -} \ No newline at end of file +} diff --git a/client/styles/components/_about.scss b/client/styles/components/_about.scss index 1ca80ea2..25e916ea 100644 --- a/client/styles/components/_about.scss +++ b/client/styles/components/_about.scss @@ -1,33 +1,3 @@ -.about { - @extend %modal; - display: flex; - flex-wrap: wrap; - flex-flow: column; - width: #{720 / $base-font-size}rem; - outline: none; - & a { - color: $form-navigation-options-color; - } -} - -.about__header { - display: flex; - justify-content: space-between; - padding-top: #{12 / $base-font-size}rem; - padding-right: #{14 / $base-font-size}rem; - padding-bottom: #{20 / $base-font-size}rem; - padding-left: #{21 / $base-font-size}rem; -} - -.about__header-title { - font-size: #{38 / $base-font-size}rem; - font-weight: normal; -} - -.about__exit-button { - @include icon(); -} - .about__logo { @include themify() { & path { @@ -67,10 +37,15 @@ display: flex; flex-direction: row; justify-content: space-between; + flex-wrap: wrap; padding-top: #{17 / $base-font-size}rem; padding-right: #{78 / $base-font-size}rem; padding-bottom: #{20 / $base-font-size}rem; padding-left: #{20 / $base-font-size}rem; + width: #{720 / $base-font-size}rem; + & a { + color: $form-navigation-options-color; + } } .about__content-column { @@ -110,6 +85,7 @@ padding-right: #{20 / $base-font-size}rem; padding-bottom: #{21 / $base-font-size}rem; padding-left: #{291 / $base-font-size}rem; + width: 100%; } .about__footer-list { diff --git a/client/styles/components/_asset-list.scss b/client/styles/components/_asset-list.scss new file mode 100644 index 00000000..5b716170 --- /dev/null +++ b/client/styles/components/_asset-list.scss @@ -0,0 +1,56 @@ +.asset-table-container { + flex: 1 1 0%; + overflow-y: scroll; + max-width: 100%; + width: #{1000 / $base-font-size}rem; + min-height: #{400 / $base-font-size}rem; +} + +.asset-table { + width: 100%; + padding: #{10 / $base-font-size}rem #{20 / $base-font-size}rem; + max-height: 100%; + border-spacing: 0; + & .asset-list__delete-column { + width: #{23 / $base-font-size}rem; + } + + & thead { + font-size: #{12 / $base-font-size}rem; + @include themify() { + color: getThemifyVariable('inactive-text-color') + } + } + + & th { + height: #{32 / $base-font-size}rem; + font-weight: normal; + } +} + +.asset-table__row { + margin: #{10 / $base-font-size}rem; + height: #{72 / $base-font-size}rem; + font-size: #{16 / $base-font-size}rem; + + &:nth-child(odd) { + @include themify() { + background: getThemifyVariable('console-header-background-color'); + } + } + + & a { + @include themify() { + color: getThemifyVariable('primary-text-color'); + } + } + + & td:first-child { + padding-left: #{10 / $base-font-size}rem; + } +} + +.asset-table__empty { + text-align: center; + font-size: #{16 / $base-font-size}rem; +} diff --git a/client/styles/components/_keyboard-shortcuts.scss b/client/styles/components/_keyboard-shortcuts.scss new file mode 100644 index 00000000..66963e91 --- /dev/null +++ b/client/styles/components/_keyboard-shortcuts.scss @@ -0,0 +1,20 @@ +.keyboard-shortcuts { + padding: #{20 / $base-font-size}rem; + padding-bottom: #{40 / $base-font-size}rem; + width: #{450 / $base-font-size}rem; +} + +.keyboard-shortcut-item { + display: flex; + & + & { + margin-top: #{10 / $base-font-size}rem; + } + align-items: baseline; +} + +.keyboard-shortcut__command { + width: 50%; + font-weight: bold; + text-align: right; + padding-right: #{10 / $base-font-size}rem; +} diff --git a/client/styles/components/_modal.scss b/client/styles/components/_modal.scss index 470509af..06830684 100644 --- a/client/styles/components/_modal.scss +++ b/client/styles/components/_modal.scss @@ -48,66 +48,3 @@ text-align: center; margin: #{20 / $base-font-size}rem 0; } - -.uploader { - min-height: #{200 / $base-font-size}rem; - width: 100%; - text-align: center; -} - -.share-modal { - @extend %modal; - padding: #{20 / $base-font-size}rem; - width: #{500 / $base-font-size}rem; -} - -.share-modal__header { - display: flex; - justify-content: space-between; -} - -.share-modal__section { - width: 100%; - display: flex; - align-items: center; - padding: #{10 / $base-font-size}rem 0; -} - -.share-modal__label { - width: #{86 / $base-font-size}rem; -} - -.share-modal__input { - flex: 1; -} - -.keyboard-shortcuts { - @extend %modal; - padding: #{20 / $base-font-size}rem; - width: #{450 / $base-font-size}rem; -} - -.keyboard-shortcuts__header { - display: flex; - justify-content: space-between; - margin-bottom: #{20 / $base-font-size}rem; -} - -.keyboard-shortcuts__close { - @include icon(); -} - -.keyboard-shortcut-item { - display: flex; - & + & { - margin-top: #{10 / $base-font-size}rem; - } - align-items: baseline; -} - -.keyboard-shortcut__command { - width: 50%; - font-weight: bold; - text-align: right; - padding-right: #{10 / $base-font-size}rem; -} diff --git a/client/styles/components/_overlay.scss b/client/styles/components/_overlay.scss index 9d952969..690d1c7e 100644 --- a/client/styles/components/_overlay.scss +++ b/client/styles/components/_overlay.scss @@ -9,10 +9,31 @@ overflow-y: hidden; } -.overlay-content { +.overlay__content { height: 100%; width: 100%; display: flex; justify-content: center; align-items: center; +} + +.overlay__body { + @extend %modal; + display: flex; + flex-wrap: wrap; + flex-flow: column; + max-height: 80%; + max-width: 65%; +} + +.overlay__header { + display: flex; + justify-content: space-between; + padding: #{40 / $base-font-size}rem #{20 / $base-font-size}rem #{30 / $base-font-size}rem #{20 / $base-font-size}rem; + flex: 1 0 auto; +} + +.overlay__close-button { + @include icon(); + padding: #{12 / $base-font-size}rem #{16 / $base-font-size}rem; } \ No newline at end of file diff --git a/client/styles/components/_share.scss b/client/styles/components/_share.scss new file mode 100644 index 00000000..a9b72c7e --- /dev/null +++ b/client/styles/components/_share.scss @@ -0,0 +1,24 @@ +.share-modal { + padding: #{20 / $base-font-size}rem; + width: #{500 / $base-font-size}rem; +} + +.share-modal__header { + display: flex; + justify-content: space-between; +} + +.share-modal__section { + width: 100%; + display: flex; + align-items: center; + padding: #{10 / $base-font-size}rem 0; +} + +.share-modal__label { + width: #{86 / $base-font-size}rem; +} + +.share-modal__input { + flex: 1; +} \ No newline at end of file diff --git a/client/styles/components/_sketch-list.scss b/client/styles/components/_sketch-list.scss index 9775b04c..034ab59b 100644 --- a/client/styles/components/_sketch-list.scss +++ b/client/styles/components/_sketch-list.scss @@ -1,25 +1,9 @@ -.sketch-list { - @extend %modal; - display: flex; - flex-wrap: wrap; - flex-flow: column; - width: #{1000 / $base-font-size}rem; - height: 80%; - outline: none; -} - -.sketch-list__header { - display: flex; - justify-content: space-between; -} - -.sketch-list__header-title { - padding: #{40 / $base-font-size}rem #{16 / $base-font-size}rem #{12 / $base-font-size}rem #{21 / $base-font-size}rem; -} .sketches-table-container { - flex: 1 0 0%; + flex: 1 1 0%; overflow-y: scroll; + max-width: 100%; + width: #{1000 / $base-font-size}rem; } .sketches-table { @@ -67,11 +51,6 @@ font-weight: normal; } -.sketch-list__exit-button { - @include icon(); - margin: #{12 / $base-font-size}rem #{16 / $base-font-size}rem; -} - .visibility-toggle .sketch-list__trash-button { @extend %hidden-element; width:#{20 / $base-font-size}rem; diff --git a/client/styles/components/_uploader.scss b/client/styles/components/_uploader.scss index e6d8bcee..695e84da 100644 --- a/client/styles/components/_uploader.scss +++ b/client/styles/components/_uploader.scss @@ -1,3 +1,9 @@ .dropzone { color: $primary-text-color; +} + +.uploader { + min-height: #{200 / $base-font-size}rem; + width: 100%; + text-align: center; } \ No newline at end of file diff --git a/client/styles/main.scss b/client/styles/main.scss index fe97261e..abe5a93b 100644 --- a/client/styles/main.scss +++ b/client/styles/main.scss @@ -34,6 +34,9 @@ @import 'components/error-modal'; @import 'components/preview-frame'; @import 'components/help-modal'; +@import 'components/share'; +@import 'components/asset-list'; +@import 'components/keyboard-shortcuts'; @import 'layout/ide'; @import 'layout/fullscreen'; diff --git a/package.json b/package.json index 544f1764..eaeec4a2 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "passport": "^0.3.2", "passport-github": "^1.1.0", "passport-local": "^1.0.0", + "pretty-bytes": "^4.0.2", "project-name-generator": "^2.1.3", "pug": "^2.0.0-beta6", "q": "^1.4.1", diff --git a/server/controllers/aws.controller.js b/server/controllers/aws.controller.js index a22be355..2b23e21f 100644 --- a/server/controllers/aws.controller.js +++ b/server/controllers/aws.controller.js @@ -1,6 +1,8 @@ import uuid from 'node-uuid'; import policy from 's3-policy'; import s3 from 's3'; +import { getProjectsForUserId } from './project.controller'; +import { findUserByUsername } from './user.controller'; const client = s3.createClient({ maxAsyncS3: 20, @@ -104,3 +106,45 @@ export function copyObjectInS3(req, res) { res.json({ url: `${s3Bucket}${userId}/${newFilename}` }); }); } + +export function listObjectsInS3ForUser(req, res) { + const username = req.params.username; + findUserByUsername(username, (user) => { + const userId = user.id; + const params = { + s3Params: { + Bucket: `${process.env.S3_BUCKET}`, + Prefix: `${userId}/` + } + }; + let assets = []; + const list = client.listObjects(params) + .on('data', (data) => { + assets = assets.concat(data["Contents"].map((object) => { + return { key: object["Key"], size: object["Size"] }; + })); + }) + .on('end', () => { + const projectAssets = []; + getProjectsForUserId(userId).then((projects) => { + projects.forEach((project) => { + project.files.forEach((file) => { + if (!file.url) return; + + const foundAsset = assets.find((asset) => file.url.includes(asset.key)); + if (!foundAsset) return; + projectAssets.push({ + name: file.name, + sketchName: project.name, + sketchId: project.id, + url: file.url, + key: foundAsset.key, + size: foundAsset.size + }); + }); + }); + res.json({assets: projectAssets}); + }); + }); + }); +} diff --git a/server/controllers/project.controller.js b/server/controllers/project.controller.js index cdfb0ac1..8814740a 100644 --- a/server/controllers/project.controller.js +++ b/server/controllers/project.controller.js @@ -121,12 +121,28 @@ export function deleteProject(req, res) { }); } -export function getProjects(req, res) { - if (req.user) { - Project.find({ user: req.user._id }) // eslint-disable-line no-underscore-dangle +export function getProjectsForUserId(userId) { + return new Promise((resolve, reject) => { + Project.find({ user: userId }) .sort('-createdAt') .select('name files id createdAt updatedAt') .exec((err, projects) => { + if (err) { + console.log(err); + } + resolve(projects); + }); + }); +} + +export function getProjectsForUserName(username) { + +} + +export function getProjects(req, res) { + if (req.user) { + getProjectsForUserId(req.user._id) + .then((projects) => { res.json(projects); }); } else { @@ -142,7 +158,7 @@ export function getProjectsForUser(req, res) { res.status(404).json({ message: 'User with that username does not exist.' }); return; } - Project.find({ user: user._id }) // eslint-disable-line no-underscore-dangle + Project.find({ user: user._id }) .sort('-createdAt') .select('name files id createdAt updatedAt') .exec((innerErr, projects) => res.json(projects)); diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index ac273d34..2e002399 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -15,6 +15,21 @@ const random = (done) => { }); }; +export function findUserByUsername(username, cb) { + User.findOne({ username }, + (err, user) => { + cb(user); + }); +} + +export function createUser(req, res, next) { + const user = new User({ + username: req.body.username, + email: req.body.email, + password: req.body.password + }); +}; + const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours export function createUser(req, res, next) { diff --git a/server/routes/aws.routes.js b/server/routes/aws.routes.js index 85daddf7..e31457a6 100644 --- a/server/routes/aws.routes.js +++ b/server/routes/aws.routes.js @@ -6,5 +6,6 @@ const router = new Router(); router.route('/S3/sign').post(AWSController.signS3); router.route('/S3/copy').post(AWSController.copyObjectInS3); router.route('/S3/:object_key').delete(AWSController.deleteObjectFromS3); +router.route('/S3/:username/objects').get(AWSController.listObjectsInS3ForUser); export default router; diff --git a/server/routes/server.routes.js b/server/routes/server.routes.js index 7bd944a2..f4ec6bf2 100644 --- a/server/routes/server.routes.js +++ b/server/routes/server.routes.js @@ -58,6 +58,12 @@ router.route('/:username/sketches').get((req, res) => { )); }); +router.route('/:username/assets').get((req, res) => { + userExists(req.params.username, exists => ( + exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) + )); +}); + router.route('/:username/account').get((req, res) => { userExists(req.params.username, exists => ( exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) From 3b36cd1e9cd5b0e990a203b77a0d8e884298038b Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Tue, 11 Jul 2017 11:50:23 -0400 Subject: [PATCH 16/24] fix asset migration script --- server/migrations/s3UnderUser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/migrations/s3UnderUser.js b/server/migrations/s3UnderUser.js index a0fbb967..107d524e 100644 --- a/server/migrations/s3UnderUser.js +++ b/server/migrations/s3UnderUser.js @@ -29,7 +29,7 @@ Project.find({}, (err, projects) => { if (!project.user) return; const userId = project.user.valueOf(); project.files.forEach((file, fileIndex) => { - if (file.url && file.url.includes(process.env.S3_BUCKET)) { + if (file.url && file.url.includes(process.env.S3_BUCKET) && !file.url.includes(userId)) { const key = file.url.split('/').pop(); console.log(key); const params = { From 0bac435c729b8d129eede9213b0019c4de512923 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Tue, 11 Jul 2017 11:51:56 -0400 Subject: [PATCH 17/24] debug asset migration --- server/migrations/s3UnderUser.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/migrations/s3UnderUser.js b/server/migrations/s3UnderUser.js index 107d524e..d56bd205 100644 --- a/server/migrations/s3UnderUser.js +++ b/server/migrations/s3UnderUser.js @@ -30,6 +30,7 @@ Project.find({}, (err, projects) => { const userId = project.user.valueOf(); project.files.forEach((file, fileIndex) => { if (file.url && file.url.includes(process.env.S3_BUCKET) && !file.url.includes(userId)) { + console.log(file.url); const key = file.url.split('/').pop(); console.log(key); const params = { From 57402cca8b423d06b977d23b2f79c2ca6ff0a45d Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Tue, 11 Jul 2017 11:53:38 -0400 Subject: [PATCH 18/24] catch migration error --- server/migrations/s3UnderUser.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/server/migrations/s3UnderUser.js b/server/migrations/s3UnderUser.js index d56bd205..289d212e 100644 --- a/server/migrations/s3UnderUser.js +++ b/server/migrations/s3UnderUser.js @@ -30,7 +30,6 @@ Project.find({}, (err, projects) => { const userId = project.user.valueOf(); project.files.forEach((file, fileIndex) => { if (file.url && file.url.includes(process.env.S3_BUCKET) && !file.url.includes(userId)) { - console.log(file.url); const key = file.url.split('/').pop(); console.log(key); const params = { @@ -38,16 +37,20 @@ Project.find({}, (err, projects) => { CopySource: `${process.env.S3_BUCKET}/${key}`, Key: `${userId}/${key}` }; - client.moveObject(params) - .on('err', (err) => { - console.log(err); - }) - .on('end', () => { - file.url = `https://s3-${process.env.AWS_REGION}.amazonaws.com/${process.env.S3_BUCKET}/${userId}/${key}`; - project.save((err, savedProject) => { - console.log(`updated file ${key}`); + try { + client.moveObject(params) + .on('err', (err) => { + console.log(err); + }) + .on('end', () => { + file.url = `https://s3-${process.env.AWS_REGION}.amazonaws.com/${process.env.S3_BUCKET}/${userId}/${key}`; + project.save((err, savedProject) => { + console.log(`updated file ${key}`); + }); }); - }); + } catch(e) { + console.log(e); + } } }); }); From a04e1b0bc483cd0e58cc65f7c6cb03b75894da24 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Tue, 11 Jul 2017 12:08:57 -0400 Subject: [PATCH 19/24] update version of pretty-bytes to one that works with webpack --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eaeec4a2..1d77b9c0 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "passport": "^0.3.2", "passport-github": "^1.1.0", "passport-local": "^1.0.0", - "pretty-bytes": "^4.0.2", + "pretty-bytes": "^3.0.1", "project-name-generator": "^2.1.3", "pug": "^2.0.0-beta6", "q": "^1.4.1", From 4684feaff6b01516fa2fb0d907dc66cda0f8c35f Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Fri, 14 Jul 2017 15:35:02 -0400 Subject: [PATCH 20/24] fix #394 --- server/controllers/user.controller.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index 2e002399..415547ee 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -22,14 +22,6 @@ export function findUserByUsername(username, cb) { }); } -export function createUser(req, res, next) { - const user = new User({ - username: req.body.username, - email: req.body.email, - password: req.body.password - }); -}; - const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours export function createUser(req, res, next) { From b384fdc6ba7341bba4397072ef0fdbb5c294f28d Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Mon, 17 Jul 2017 17:34:11 -0400 Subject: [PATCH 21/24] remove console errors and extraneous console logs --- client/modules/IDE/components/Editor.jsx | 1 - client/modules/IDE/pages/IDEView.jsx | 3 ++- client/modules/IDE/reducers/project.js | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/modules/IDE/components/Editor.jsx b/client/modules/IDE/components/Editor.jsx index d4699cf8..0d365c97 100644 --- a/client/modules/IDE/components/Editor.jsx +++ b/client/modules/IDE/components/Editor.jsx @@ -176,7 +176,6 @@ class Editor extends React.Component { } initializeDocuments(files) { - console.log('calling initialize documents'); this._docs = {}; files.forEach((file) => { if (file.name !== 'root') { diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 1d503291..ca6d22d3 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -263,6 +263,7 @@ class IDEView extends React.Component { setLintWarning={this.props.setLintWarning} textOutput={this.props.preferences.textOutput} gridOutput={this.props.preferences.gridOutput} + soundOutput={this.props.preferences.soundOutput} setTextOutput={this.props.setTextOutput} setGridOutput={this.props.setGridOutput} setSoundOutput={this.props.setSoundOutput} @@ -564,7 +565,7 @@ IDEView.propTypes = { infiniteLoop: PropTypes.bool.isRequired, previewIsRefreshing: PropTypes.bool.isRequired, infiniteLoopMessage: PropTypes.string.isRequired, - projectSavedTime: PropTypes.string.isRequired, + projectSavedTime: PropTypes.string, previousPath: PropTypes.string.isRequired, justOpenedProject: PropTypes.bool.isRequired, errorType: PropTypes.string, diff --git a/client/modules/IDE/reducers/project.js b/client/modules/IDE/reducers/project.js index eb1de631..c3572381 100644 --- a/client/modules/IDE/reducers/project.js +++ b/client/modules/IDE/reducers/project.js @@ -8,6 +8,7 @@ const initialState = () => { return { name: generatedName, serveSecure: isSecurePage(), + updatedAt: '' }; }; From e987e8f483561a69e8346fc1f649ac5f33a89b58 Mon Sep 17 00:00:00 2001 From: Zach Rispoli Date: Wed, 19 Jul 2017 13:56:52 -0400 Subject: [PATCH 22/24] Bundle libraries when project is downloaded as zip (New approach) (#376) * External libraries are bundled with zip when project is downloaded (#44) * Fix linting errors * Add a check for valid URLs before trying to bundle a library into project * Add is-url lib to package.json --- package.json | 1 + server/controllers/project.controller.js | 52 ++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1d77b9c0..bb328c95 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "fs-promise": "^1.0.0", "htmlhint": "^0.9.13", "is_js": "^0.9.0", + "is-url": "^1.2.2", "js-beautify": "^1.6.4", "jsdom": "^9.8.3", "jshint": "^2.9.4", diff --git a/server/controllers/project.controller.js b/server/controllers/project.controller.js index 8814740a..911eab1e 100644 --- a/server/controllers/project.controller.js +++ b/server/controllers/project.controller.js @@ -1,6 +1,8 @@ import archiver from 'archiver'; import request from 'request'; import moment from 'moment'; +import isUrl from 'is-url'; +import jsdom, { serializeDocument } from 'jsdom'; import Project from '../models/project'; import User from '../models/user'; import { deleteObjectsFromS3, getObjectKey } from './aws.controller'; @@ -100,8 +102,7 @@ function deleteFilesFromS3(files) { } return false; }) - .map(file => getObjectKey(file.url)) - ); + .map(file => getObjectKey(file.url))); } export function deleteProject(req, res) { @@ -169,6 +170,48 @@ export function getProjectsForUser(req, res) { } } +function bundleExternalLibs(project, zip, callback) { + const rootFile = project.files.find(file => file.name === 'root'); + const indexHtml = project.files.find(file => file.name === 'index.html'); + let numScriptsResolved = 0; + let numScriptTags = 0; + + function resolveScriptTagSrc(scriptTag, document) { + const path = scriptTag.src.split('/'); + const filename = path[path.length - 1]; + const src = scriptTag.src; + + if (!isUrl(src)) { + numScriptsResolved += 1; + return; + } + + request({ method: 'GET', url: src, encoding: null }, (err, response, body) => { + if (err) { + console.log(err); + } else { + zip.append(body, { name: filename }); + scriptTag.src = filename; + } + + numScriptsResolved += 1; + if (numScriptsResolved === numScriptTags) { + indexHtml.content = serializeDocument(document); + callback(); + } + }); + } + + jsdom.env(indexHtml.content, (innerErr, window) => { + const indexHtmlDoc = window.document; + const scriptTags = indexHtmlDoc.getElementsByTagName('script'); + numScriptTags = scriptTags.length; + for (let i = 0; i < numScriptTags; i += 1) { + resolveScriptTagSrc(scriptTags[i], indexHtmlDoc); + } + }); +} + function buildZip(project, req, res) { const zip = archiver('zip'); const rootFile = project.files.find(file => file.name === 'root'); @@ -208,7 +251,10 @@ function buildZip(project, req, res) { } } } - addFileToZip(rootFile, '/'); + + bundleExternalLibs(project, zip, () => { + addFileToZip(rootFile, '/'); + }); } export function downloadProjectAsZip(req, res) { From 983248ccb737439de350299794700237ebe68a84 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Mon, 24 Jul 2017 11:12:11 -0400 Subject: [PATCH 23/24] fixes #402 --- client/modules/IDE/components/SketchList.jsx | 88 ++++++++++---------- client/styles/components/_asset-list.scss | 2 +- client/styles/components/_sketch-list.scss | 8 +- 3 files changed, 53 insertions(+), 45 deletions(-) diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index 7094d72b..478bc265 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -20,49 +20,53 @@ class SketchList extends React.Component { const username = this.props.username !== undefined ? this.props.username : this.props.user.username; return (
    - - - - - - - - - - - {this.props.sketches.map(sketch => - // eslint-disable-next-line - browserHistory.push(`/${username}/sketches/${sketch.id}`)} - > - - - - + { this.props.sketches.length === 0 && +

    No sketches.

    + } + { this.props.sketches.length > 0 && +
    SketchDate createdDate updated
    - {(() => { // eslint-disable-line - if (this.props.username === this.props.user.username || this.props.username === undefined) { - return ( - - ); - } - })()} - {sketch.name}{moment(sketch.createdAt).format('MMM D, YYYY h:mm A')}{moment(sketch.updatedAt).format('MMM D, YYYY h:mm A')}
    + + + + + + - )} - -
    SketchDate createdDate updated
    + + + {this.props.sketches.map(sketch => + // eslint-disable-next-line + browserHistory.push(`/${username}/sketches/${sketch.id}`)} + > + + {(() => { // eslint-disable-line + if (this.props.username === this.props.user.username || this.props.username === undefined) { + return ( + + ); + } + })()} + + {sketch.name} + {moment(sketch.createdAt).format('MMM D, YYYY h:mm A')} + {moment(sketch.updatedAt).format('MMM D, YYYY h:mm A')} + + )} + + }
    ); } diff --git a/client/styles/components/_asset-list.scss b/client/styles/components/_asset-list.scss index 5b716170..76013deb 100644 --- a/client/styles/components/_asset-list.scss +++ b/client/styles/components/_asset-list.scss @@ -1,5 +1,5 @@ .asset-table-container { - flex: 1 1 0%; + // flex: 1 1 0%; overflow-y: scroll; max-width: 100%; width: #{1000 / $base-font-size}rem; diff --git a/client/styles/components/_sketch-list.scss b/client/styles/components/_sketch-list.scss index 034ab59b..1369699c 100644 --- a/client/styles/components/_sketch-list.scss +++ b/client/styles/components/_sketch-list.scss @@ -1,9 +1,8 @@ - .sketches-table-container { - flex: 1 1 0%; overflow-y: scroll; max-width: 100%; width: #{1000 / $base-font-size}rem; + min-height: #{400 / $base-font-size}rem; } .sketches-table { @@ -72,3 +71,8 @@ } } } + +.sketches-table__empty { + text-align: center; + font-size: #{16 / $base-font-size}rem; +} From fa98306b9a036a187a8aee711643df1cb63cd04e Mon Sep 17 00:00:00 2001 From: Katyayani Singh Date: Fri, 28 Jul 2017 21:08:47 +0530 Subject: [PATCH 24/24] Update Find styling to new design (#401) * Update Find styling * Update placeholder text --- client/styles/abstracts/_variables.scss | 3 +++ client/styles/components/_editor.scss | 15 ++++++++++++--- client/utils/codemirror-search.js | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/client/styles/abstracts/_variables.scss b/client/styles/abstracts/_variables.scss index 6fc8c9cf..122c9df8 100644 --- a/client/styles/abstracts/_variables.scss +++ b/client/styles/abstracts/_variables.scss @@ -44,6 +44,7 @@ $themes: ( input-text-color: #333, input-border-color: #b5b5b5, about-list-text-color: #4a4a4a, + search-background-color: #ebebeb ), dark: ( logo-color: $p5js-pink, @@ -79,6 +80,7 @@ $themes: ( input-text-color: #333, input-border-color: #b5b5b5, about-list-text-color: #f4f4f4, + search-background-color: #ebebeb ), contrast: ( logo-color: $yellow, @@ -113,6 +115,7 @@ $themes: ( input-text-color: #333, input-border-color: #b5b5b5, about-list-text-color: #f4f4f4, + search-background-color: $white ) ); diff --git a/client/styles/components/_editor.scss b/client/styles/components/_editor.scss index 75647316..194264d0 100644 --- a/client/styles/components/_editor.scss +++ b/client/styles/components/_editor.scss @@ -85,13 +85,15 @@ */ .CodeMirror-dialog { - position: absolute; + position: fixed; top: 0; - right: 0; + left: 50%; + margin-left: - #{365/2/$base-font-size}rem; z-index: 10; - min-width: 365px; + width: 100%; + max-width: #{365 / $base-font-size}rem; font-family: Montserrat, sans-serif; @@ -118,6 +120,13 @@ display: block; width: 100%; margin-bottom: #{12 / $base-font-size}rem; + @include themify() { + background-color: getThemifyVariable('search-background-color'); + border: solid 0.5px getThemifyVariable('button-border-color'); + } + &:focus { + background-color: $white; + } } .CodeMirror-search-count { diff --git a/client/utils/codemirror-search.js b/client/utils/codemirror-search.js index 340bb74a..32c52a63 100644 --- a/client/utils/codemirror-search.js +++ b/client/utils/codemirror-search.js @@ -223,7 +223,7 @@ export default function(CodeMirror) { var queryDialog = `

    Find

    - +