Skip to content

Commit ae668f6

Browse files
andrewncatarak
authored andcommitted
HTTPS UI switch (#335)
* Checkbox to toggle project's serveSecure flag This doesn't yet persist or reload the page. * Help button that shows modal to explain feature * Extracts protocol redirection to helper * Returns promise from saveProject() action to allow chaining * Setting serveSecure flag on project redirects after saving project * Set serveSecure on Project model in API and client * Redirect to correct protocol when project is loaded
1 parent 32d3f7a commit ae668f6

File tree

14 files changed

+300
-58
lines changed

14 files changed

+300
-58
lines changed

client/components/forceProtocol.jsx

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
1-
import React, { PropTypes } from 'react';
1+
import React from 'react';
22
import { format, parse } from 'url';
33

4+
const findCurrentProtocol = () => (
5+
parse(window.location.href).protocol
6+
);
7+
8+
const redirectToProtocol = (protocol, { appendSource, disable = false } = {}) => {
9+
const currentProtocol = findCurrentProtocol();
10+
11+
if (protocol !== currentProtocol) {
12+
if (disable === true) {
13+
console.info(`forceProtocol: would have redirected from "${currentProtocol}" to "${protocol}"`);
14+
} else {
15+
const url = parse(window.location.href, true /* parse query string */);
16+
url.protocol = protocol;
17+
if (appendSource === true) {
18+
url.query.source = currentProtocol;
19+
}
20+
window.location = format(url);
21+
}
22+
}
23+
};
24+
425
/**
526
* A Higher Order Component that forces the protocol to change on mount
627
*
@@ -14,29 +35,12 @@ const forceProtocol = ({ targetProtocol = 'https', sourceProtocol, disable = fal
1435
static propTypes = {}
1536

1637
componentDidMount() {
17-
this.redirectToProtocol(targetProtocol, { appendSource: true });
38+
redirectToProtocol(targetProtocol, { appendSource: true, disable });
1839
}
1940

2041
componentWillUnmount() {
2142
if (sourceProtocol != null) {
22-
this.redirectToProtocol(sourceProtocol, { appendSource: false });
23-
}
24-
}
25-
26-
redirectToProtocol(protocol, { appendSource }) {
27-
const currentProtocol = parse(window.location.href).protocol;
28-
29-
if (protocol !== currentProtocol) {
30-
if (disable === true) {
31-
console.info(`forceProtocol: would have redirected from "${currentProtocol}" to "${protocol}"`);
32-
} else {
33-
const url = parse(window.location.href, true /* parse query string */);
34-
url.protocol = protocol;
35-
if (appendSource === true) {
36-
url.query.source = currentProtocol;
37-
}
38-
window.location = format(url);
39-
}
43+
redirectToProtocol(sourceProtocol, { appendSource: false, disable });
4044
}
4145
}
4246

@@ -65,6 +69,8 @@ const findSourceProtocol = (state, location) => {
6569

6670
export default forceProtocol;
6771
export {
72+
findCurrentProtocol,
6873
findSourceProtocol,
74+
redirectToProtocol,
6975
protocols,
7076
};

client/constants.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const AUTH_ERROR = 'AUTH_ERROR';
2626
export const SETTINGS_UPDATED = 'SETTINGS_UPDATED';
2727

2828
export const SET_PROJECT_NAME = 'SET_PROJECT_NAME';
29+
export const SET_SERVE_SECURE = 'SET_SERVE_SECURE';
2930

3031
export const PROJECT_SAVE_SUCCESS = 'PROJECT_SAVE_SUCCESS';
3132
export const PROJECT_SAVE_FAIL = 'PROJECT_SAVE_FAIL';
@@ -113,3 +114,6 @@ export const HIDE_ERROR_MODAL = 'HIDE_ERROR_MODAL';
113114

114115
export const PERSIST_STATE = 'PERSIST_STATE';
115116
export const CLEAR_PERSISTED_STATE = 'CLEAR_PERSISTED_STATE';
117+
118+
export const SHOW_HELP_MODAL = 'SHOW_HELP_MODAL';
119+
export const HIDE_HELP_MODAL = 'HIDE_HELP_MODAL';

client/images/help.svg

Lines changed: 7 additions & 0 deletions
Loading

client/modules/IDE/actions/ide.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,16 @@ export function hideErrorModal() {
220220
type: ActionTypes.HIDE_ERROR_MODAL
221221
};
222222
}
223+
224+
export function showHelpModal(helpType) {
225+
return {
226+
type: ActionTypes.SHOW_HELP_MODAL,
227+
helpType
228+
};
229+
}
230+
231+
export function hideHelpModal() {
232+
return {
233+
type: ActionTypes.HIDE_HELP_MODAL
234+
};
235+
}

client/modules/IDE/actions/project.js

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,18 @@ import { setUnsavedChanges,
99
resetJustOpenedProject,
1010
showErrorModal } from './ide';
1111
import { clearState, saveState } from '../../../persistState';
12+
import { redirectToProtocol, protocols } from '../../../components/forceProtocol';
1213

1314
const ROOT_URL = process.env.API_URL;
1415

1516
export function setProject(project) {
17+
const targetProtocol = project.serveSecure === true ?
18+
protocols.https :
19+
protocols.http;
20+
21+
// This will not reload if on same protocol
22+
redirectToProtocol(targetProtocol);
23+
1624
return {
1725
type: ActionTypes.SET_PROJECT,
1826
project,
@@ -66,12 +74,12 @@ export function saveProject(autosave = false) {
6674
return (dispatch, getState) => {
6775
const state = getState();
6876
if (state.user.id && state.project.owner && state.project.owner.id !== state.user.id) {
69-
return;
77+
return Promise.reject();
7078
}
7179
const formParams = Object.assign({}, state.project);
7280
formParams.files = [...state.files];
7381
if (state.project.id) {
74-
axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true })
82+
return axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true })
7583
.then((response) => {
7684
dispatch(setUnsavedChanges(false));
7785
console.log(response.data);
@@ -103,41 +111,41 @@ export function saveProject(autosave = false) {
103111
});
104112
}
105113
});
106-
} else {
107-
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
108-
.then((response) => {
109-
dispatch(setUnsavedChanges(false));
110-
dispatch(setProject(response.data));
111-
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
112-
dispatch({
113-
type: ActionTypes.NEW_PROJECT,
114-
project: response.data,
115-
owner: response.data.user,
116-
files: response.data.files
117-
});
118-
if (!autosave) {
119-
if (state.preferences.autosave) {
120-
dispatch(showToast(5500));
121-
dispatch(setToastText('Project saved.'));
122-
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
123-
dispatch(resetJustOpenedProject());
124-
} else {
125-
dispatch(showToast(1500));
126-
dispatch(setToastText('Project saved.'));
127-
}
128-
}
129-
})
130-
.catch((response) => {
131-
if (response.status === 403) {
132-
dispatch(showErrorModal('staleSession'));
114+
}
115+
116+
return axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
117+
.then((response) => {
118+
dispatch(setUnsavedChanges(false));
119+
dispatch(setProject(response.data));
120+
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
121+
dispatch({
122+
type: ActionTypes.NEW_PROJECT,
123+
project: response.data,
124+
owner: response.data.user,
125+
files: response.data.files
126+
});
127+
if (!autosave) {
128+
if (state.preferences.autosave) {
129+
dispatch(showToast(5500));
130+
dispatch(setToastText('Project saved.'));
131+
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
132+
dispatch(resetJustOpenedProject());
133133
} else {
134-
dispatch({
135-
type: ActionTypes.PROJECT_SAVE_FAIL,
136-
error: response.data
137-
});
134+
dispatch(showToast(1500));
135+
dispatch(setToastText('Project saved.'));
138136
}
139-
});
140-
}
137+
}
138+
})
139+
.catch((response) => {
140+
if (response.status === 403) {
141+
dispatch(showErrorModal('staleSession'));
142+
} else {
143+
dispatch({
144+
type: ActionTypes.PROJECT_SAVE_FAIL,
145+
error: response.data
146+
});
147+
}
148+
});
141149
};
142150
}
143151

@@ -249,6 +257,24 @@ export function cloneProject() {
249257
};
250258
}
251259

260+
export function setServeSecure(serveSecure, { redirect = true } = {}) {
261+
return (dispatch, getState) => {
262+
dispatch({
263+
type: ActionTypes.SET_SERVE_SECURE,
264+
serveSecure
265+
});
266+
267+
if (redirect === true) {
268+
dispatch(saveProject(false /* autosave */))
269+
.then(
270+
() => redirectToProtocol(serveSecure === true ? protocols.https : protocols.http)
271+
);
272+
}
273+
274+
return null;
275+
};
276+
}
277+
252278
export function showEditProjectName() {
253279
return {
254280
type: ActionTypes.SHOW_EDIT_PROJECT_NAME
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React, { PropTypes } from 'react';
2+
import InlineSVG from 'react-inlinesvg';
3+
4+
const exitUrl = require('../../../images/exit.svg');
5+
6+
const helpContent = {
7+
serveSecure: {
8+
title: 'Serve over HTTPS',
9+
body: (
10+
<div>
11+
<p>Use the checkbox to choose whether this sketch should be loaded using HTTPS or HTTP.</p>
12+
<p>You should choose HTTPS if you need to:</p>
13+
<ul>
14+
<li>access a webcam or microphone</li>
15+
<li>access an API served over HTTPS</li>
16+
</ul>
17+
<p>Choose HTTP if you need to:</p>
18+
<ul>
19+
<li>access an API served over HTTP</li>
20+
</ul>
21+
</div>
22+
)
23+
}
24+
};
25+
26+
const fallbackContent = {
27+
title: 'No content for this topic',
28+
body: null,
29+
};
30+
31+
class HelpModal extends React.Component {
32+
componentDidMount() {
33+
this.shareModal.focus();
34+
}
35+
render() {
36+
const content = helpContent[this.props.type] == null ?
37+
fallbackContent :
38+
helpContent[this.props.type];
39+
40+
return (
41+
<section className="help-modal" ref={(element) => { this.shareModal = element; }} tabIndex="0">
42+
<header className="help-modal__header">
43+
<h2>{content.title}</h2>
44+
<button className="about__exit-button" onClick={this.props.closeModal}>
45+
<InlineSVG src={exitUrl} alt="Close Help Overlay" />
46+
</button>
47+
</header>
48+
<div className="help-modal__section">
49+
{content.body}
50+
</div>
51+
</section>
52+
);
53+
}
54+
}
55+
56+
HelpModal.propTypes = {
57+
type: PropTypes.string.isRequired,
58+
closeModal: PropTypes.func.isRequired,
59+
};
60+
61+
export default HelpModal;

client/modules/IDE/components/Toolbar.jsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const logoUrl = require('../../../images/p5js-logo.svg');
88
const stopUrl = require('../../../images/stop.svg');
99
const preferencesUrl = require('../../../images/preferences.svg');
1010
const editProjectNameUrl = require('../../../images/pencil.svg');
11+
const helpUrl = require('../../../images/help.svg');
1112

1213
class Toolbar extends React.Component {
1314
constructor(props) {
@@ -102,6 +103,30 @@ class Toolbar extends React.Component {
102103
Auto-refresh
103104
</label>
104105
</div>
106+
{
107+
this.props.currentUser == null ?
108+
null :
109+
<div className="toolbar__serve-secure">
110+
<input
111+
id="serve-secure"
112+
type="checkbox"
113+
checked={this.props.project.serveSecure || false}
114+
onChange={(event) => {
115+
this.props.setServeSecure(event.target.checked);
116+
}}
117+
/>
118+
<label htmlFor="serve-secure" className="toolbar__serve-secure-label">
119+
HTTPS
120+
</label>
121+
<button
122+
className="toolbar__serve-secure-help"
123+
onClick={() => this.props.showHelpModal('serveSecure')}
124+
aria-label="help"
125+
>
126+
<InlineSVG src={helpUrl} alt="Help" />
127+
</button>
128+
</div>
129+
}
105130
<div className={nameContainerClass}>
106131
<a
107132
className="toolbar__project-name"
@@ -169,13 +194,16 @@ Toolbar.propTypes = {
169194
project: PropTypes.shape({
170195
name: PropTypes.string.isRequired,
171196
isEditingName: PropTypes.bool,
172-
id: PropTypes.string
197+
id: PropTypes.string,
198+
serveSecure: PropTypes.bool,
173199
}).isRequired,
174200
showEditProjectName: PropTypes.func.isRequired,
175201
hideEditProjectName: PropTypes.func.isRequired,
202+
showHelpModal: PropTypes.func.isRequired,
176203
infiniteLoop: PropTypes.bool.isRequired,
177204
autorefresh: PropTypes.bool.isRequired,
178205
setAutorefresh: PropTypes.func.isRequired,
206+
setServeSecure: PropTypes.func.isRequired,
179207
startSketchAndRefresh: PropTypes.func.isRequired,
180208
saveProject: PropTypes.func.isRequired,
181209
currentUser: PropTypes.string,

0 commit comments

Comments
 (0)