diff --git a/bin/launch b/bin/launch index 43138adbc..018530746 100755 --- a/bin/launch +++ b/bin/launch @@ -1,6 +1,6 @@ #!/bin/bash set -eo pipefail -version="0.4" +version="0.5" MAKERVERSE_HOME=${MAKERVERSE_HOME:-${HOME}} # Figure out where the git checkout directory lives, if any: @@ -16,6 +16,7 @@ fi MAKERVERSE_GIT_DIR="${MAKERVERSE_SRC_DIR}" MAKERVERSE_LAUNCH_METHOD=${MAKERVERSE_LAUNCH_METHOD:-docker} MAKERVERSE_SETTINGS_FILE="${MAKERVERSE_HOME}/.makerverse" +MAKERVERSE_WATCH_DIR="${MAKERVERSE_WATCH_DIR:-${MAKERVERSE_HOME}/gcode}" MAKERVERSE_PORT="${MAKERVERSE_PORT:-8000}" # Scan dependency versions @@ -167,7 +168,7 @@ launch() { -p "${MAKERVERSE_PORT}:8000" \ -v /dev:/dev \ -v /var/run/docker.sock:/var/run/docker.sock \ - -v "$MAKERVERSE_HOME/gcode:/home/node/gcode" \ + -v "$MAKERVERSE_WATCH_DIR:/home/node/gcode" \ -v "$MAKERVERSE_SETTINGS_FILE:/home/node/.makerverse" \ -v "$MAKERVERSE_SRC_DIR:/home/node/makerverse" \ "$docker_tagged" diff --git a/docs/features/programs.md b/docs/features/programs.md new file mode 100644 index 000000000..9156cd690 --- /dev/null +++ b/docs/features/programs.md @@ -0,0 +1,34 @@ +--- +layout: default +title: Programs +parent: Features +nav_order: 1 +--- + +# Programs + +Makerverse runs gcode (`.nc`) programs. + +## Load + +### Upload Program + +From your workspace, press the "Upload Program" button. You will be able to select a file from your current device. + +### Watch Directory + +On the right side of the "Upload Program" button is an arrow. Clicking it lets you explore the "Watch Directory." For example, with a Web Server installation, you can instead place the `.nc` file into the folder at `$HOME/gcode` on the computer where Makerverse is installed. + +_On the Raspberry Pi image, the watch directory is `/home/pi/gcode`._ + +_*Note*: Makerverse does not (yet) support folders within the watch directory._ + +## Run + +To run the program, just press the play button (next to "Upload Program"). + +You can monitor the execution with the `G-code` widget. + +### Multiple Devices + +With a Web Server installation, you can upload a program from one computer (e.g., desktop) and then run/monitor Makerverse from another (e.g., shopfloor tablet or smartphone). diff --git a/docs/features/security.md b/docs/features/security.md new file mode 100644 index 000000000..350c287ac --- /dev/null +++ b/docs/features/security.md @@ -0,0 +1,22 @@ +--- +layout: default +title: Security +parent: Features +nav_order: 9 +--- + +# Security + +Makerverse protects access to your machine(s) with an account/log-in system. + +## Account Creation + +When you create (or log in) to your account, it prevents your Makerverse installation from being accessed by malicious parties. The Makerverse account system is powered by OpenWorkShop. + +## Offline Usage + +When you first install Makerverse and log in to your account, an internet connection is required. After that point, Makerverse will continue to work even if there is no internet available. + +## Password Recovery + +If at any point you become locked out of your account, you can reset your password using the email address you used to create the account. diff --git a/docs/installation/web-server/raspberry-pi.md b/docs/installation/web-server/raspberry-pi.md index be290a49a..c235129f2 100644 --- a/docs/installation/web-server/raspberry-pi.md +++ b/docs/installation/web-server/raspberry-pi.md @@ -12,6 +12,13 @@ This is the easiest approach. It does all the work of setting up the Docker environment (and Linux service) for you. If you prefer to set up your Raspberry Pi manually (instead of using a pre-built SD card), see the [Linux Service](/installation/web-server/linux-service/) instructions. +## Requirements + +- Raspberry Pi 3B+ (or better) +- 16 GB micro SD card (or bigger) +- Power supply for the RPi +- _Optional: keyboard, mouse, and monitor_ + ## Choose Release There are two download options in the [Latest Release](https://github.com/makermadecnc/makerverse/releases/latest/): diff --git a/package.json b/package.json index d556af06e..7f87fc55c 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "dependencies": { "@babel/polyfill": "~7.4.3", "@babel/runtime": "~7.4.3", + "@reduxjs/toolkit": "^1.4.0", "@trendmicro/react-anchor": "~0.5.6", "@trendmicro/react-breadcrumbs": "~0.5.5", "@trendmicro/react-buttons": "~1.3.1", @@ -214,6 +215,7 @@ "mousetrap": "~1.6.3", "namespace-constants": "~0.5.0", "normalize.css": "~8.0.1", + "oidc-client": "^1.10.1", "opencollective": "~1.0.3", "perfect-scrollbar": "~1.4.0", "prop-types": "~15.7.2", @@ -244,6 +246,7 @@ "react-toggle": "~4.0.2", "recompose": "~0.30.0", "redux": "~4.0.1", + "redux-oidc": "^4.0.0-beta1", "registry-auth-token": "~3.4.0", "registry-url": "~5.1.0", "rimraf": "~2.6.3", @@ -313,11 +316,12 @@ "json-loader": "~0.5.7", "mini-css-extract-plugin": "~0.6.0", "nib": "~1.1.2", + "nodemon": "~2.0.4", "optimize-css-assets-webpack-plugin": "~5.0.1", "pre-push": "~0.1.1", "progress": "~2.0.3", "react-hot-loader": "~4.8.4", - "redux-devtools": "~3.5.0", + "redux-devtools": "^3.5.0", "run-sequence": "~2.2.1", "style-loader": "~0.23.1", "stylint": "~1.5.9", @@ -325,9 +329,9 @@ "stylus": "~0.54.5", "stylus-loader": "~3.0.2", "tap": "~12.6.2", + "terser-webpack-plugin": "~1.2.3", "text-table": "~0.2.0", "transform-loader": "~0.2.4", - "terser-webpack-plugin": "~1.2.3", "url-loader": "~1.1.2", "webpack": "~4.30.0", "webpack-cli": "~3.3.1", @@ -336,8 +340,7 @@ "webpack-hot-middleware": "~2.24.3", "webpack-manifest-plugin": "~2.0.4", "webpack-node-externals": "~1.7.2", - "write-file-webpack-plugin": "~4.5.0", - "nodemon": "~2.0.4" + "write-file-webpack-plugin": "~4.5.0" }, "pre-push": [ "eslint:debug" diff --git a/src/app/api/index.js b/src/app/api/index.js index 9cf219d28..fd998b3a0 100644 --- a/src/app/api/index.js +++ b/src/app/api/index.js @@ -27,7 +27,10 @@ const noCache = (request) => { } }; -const authrequest = superagentUse(superagent); +export const apirequest = superagentUse(superagent); +apirequest.use(noCache); + +export const authrequest = superagentUse(superagent); authrequest.use(bearer); authrequest.use(noCache); @@ -35,18 +38,31 @@ authrequest.use(noCache); // Authentication // const signin = (options) => new Promise((resolve, reject) => { - const { token, name, password } = { ...options }; - - authrequest - .post('/api/signin') - .send({ token, name, password }) - .end((err, res) => { - if (err) { - reject(res); - } else { - resolve(res); - } - }); + // const { token, username, password } = { ...options }; + + // const requestedUrl = new URLSearchParams(window.location.search).get('ReturnUrl'); + // args.returnUrl = requestedUrl || (await ).url; + // const args = { username, password }; + + // fetch(`${owsApi}/auth/login`, { + // method: 'POST', + // credentials: 'include', + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify(args), + // }); + + // authrequest + // .post('/api/signin') + // .send({ token, name, password }) + // .end((err, res) => { + // if (err) { + // reject(res); + // } else { + // resolve(res); + // } + // }); }); // diff --git a/src/app/components/ProtectedRoute/index.jsx b/src/app/components/ProtectedRoute/index.jsx index aced08682..96857fc37 100644 --- a/src/app/components/ProtectedRoute/index.jsx +++ b/src/app/components/ProtectedRoute/index.jsx @@ -1,13 +1,15 @@ import React from 'react'; +// import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; import { Route, Redirect, withRouter } from 'react-router-dom'; -import * as user from 'app/lib/user'; import log from 'app/lib/log'; const ProtectedRoute = ({ component: Component, ...rest }) => ( { - if (user.isAuthenticated()) { + if (props.user) { + log.debug('User is authenticated.', props.user); return Component ? : null; } @@ -34,7 +36,19 @@ const ProtectedRoute = ({ component: Component, ...rest }) => ( ); ProtectedRoute.propTypes = { - ...withRouter.propTypes + ...withRouter.propTypes, }; -export default ProtectedRoute; +function mapStateToProps(state) { + return { + user: state.oidc.user + }; +} + +function mapDispatchToProps(dispatch) { + return { + dispatch + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(ProtectedRoute); diff --git a/src/app/containers/Login/Login.jsx b/src/app/containers/Login/Login.jsx index 47519e064..b0eaf91d2 100644 --- a/src/app/containers/Login/Login.jsx +++ b/src/app/containers/Login/Login.jsx @@ -3,15 +3,14 @@ import qs from 'qs'; import React, { PureComponent } from 'react'; import { withRouter, Redirect } from 'react-router-dom'; import Anchor from 'app/components/Anchor'; -import { Notification } from 'app/components/Notifications'; import Space from 'app/components/Space'; -import settings from 'app/config/settings'; +// import settings from 'app/config/settings'; import Workspaces from 'app/lib/workspaces'; -import promisify from 'app/lib/promisify'; +// import promisify from 'app/lib/promisify'; import auth from 'app/lib/auth'; import i18n from 'app/lib/i18n'; import log from 'app/lib/log'; -import * as user from 'app/lib/user'; +import { signup, signin, resend } from 'app/lib/user'; import store from 'app/store'; import styles from './index.styl'; @@ -22,12 +21,53 @@ class Login extends PureComponent { state = this.getDefaultState(); + processErrors = (errors) => { + const errs = this.emptyErrors; + const registering = this.state.registering; + let verificationRequired = false; + errors.forEach((err) => { + const str = err.code ? err.code.toLowerCase() : ''; + let key = 'unknown'; + if (str === 'notallowed') { + verificationRequired = true; + return; + } else if (str.includes('email')) { + key = 'email'; + } else if (str.includes('password')) { + key = 'password'; + } else if (str.includes('username')) { + key = 'username'; + } + errs[key].push(err.message ?? err); + }); + const msg = i18n._(registering ? 'Registration failed.' : 'Authentication failed.'); + this.setState({ + registered: verificationRequired, + alertMessage: verificationRequired ? null : msg, + authenticating: false, + redirectToReferrer: false, + errors: verificationRequired ? this.emptyErrors : errs, + }); + } + actions = { - showAlertMessage: (msg) => { - this.setState({ alertMessage: msg }); + handleResendVerification: (event) => { + event.preventDefault(); + this.setState({ resending: true }); + resend({ username: this.state.username }) + .then(({ success, errors }) => { + if (!success) { + this.processErrors(errors); + return; + } + this.setState({ + resent: true, + resending: false, + }); + }); }, - clearAlertMessage: () => { - this.setState({ alertMessage: '' }); + handleForgotPassword: (event) => { + event.preventDefault(); }, handleSignIn: (event) => { event.preventDefault(); @@ -35,20 +75,29 @@ class Login extends PureComponent { this.setState({ alertMessage: '', authenticating: true, - redirectToReferrer: false + redirectToReferrer: false, + errors: { ...this.state.errors }, }); - const name = this.fields.name.value; - const password = this.fields.password.value; - - user.signin({ name, password }) - .then(({ authenticated }) => promisify(next => { - if (!authenticated) { - this.setState({ - alertMessage: i18n._('Authentication failed.'), - authenticating: false, - redirectToReferrer: false - }); + const args = { + username: this.fields.username.value, + password: this.fields.password.value, + }; + + const registering = this.state.registering; + if (registering) { + args.email = this.fields.email.value; + } + + const func = registering ? signup : signin; + func(args) + .then(({ success, errors }) => { + if (!success) { + this.processErrors(errors); + return; + } + if (registering) { + this.setState({ registered: true }); return; } @@ -59,38 +108,126 @@ class Login extends PureComponent { auth.options = { query: 'token=' + token }; - next(); - })) + }) .then(Workspaces.connect) .then(() => { this.setState({ alertMessage: '', authenticating: false, - redirectToReferrer: true + redirectToReferrer: true, + errors: this.emptyErrors, }); + }) + .catch((e) => { + console.log('signup/signin error', e); }); } }; fields = { - name: null, - password: null + email: null, + username: null, + password: null, + passwordMatch: null, }; getDefaultState() { return { alertMessage: '', + username: '', + registering: false, + resending: false, authenticating: false, - redirectToReferrer: false + redirectToReferrer: false, + registered: false, + errors: this.emptyErrors, + }; + } + + onChangeEmail() { + if (!this.state.registering || this.state.hasChangedUsername) { + return; + } + this.fields.username.value = this.fields.email.value.split('@')[0]; + } + + onChangePasswords(v1) { + if (!this.state.registering || v1.length <= 0) { + this.setState({ password: v1 }); + return; + } + const v2 = this.fields.passwordMatch.value || ''; + const errs = []; + if (v1 !== v2) { + errs.push('Passwords do not match.'); + } + this.setState({ + ...this.state, + errors: { + ...this.state.errors, + passwordMatch: errs, + } + }); + } + + get emptyErrors() { + return { + email: [], + username: [], + password: [], + passwordMatch: [], + unknown: [], }; } + renderError(kind) { + const msgs = this.state.errors[kind]; + return !msgs ? '' : ( + + ); + } + + renderResend() { + if (this.state.resent) { + return The email has been re-sent.; + } + const enabled = !this.state.resending; + return ( + + ); + } + render() { const { from } = this.props.location.state || { from: { pathname: '/' } }; const state = { ...this.state }; - const actions = { ...this.actions }; - const { alertMessage, authenticating } = state; - const forgotPasswordLink = 'https://cnc.js.org/docs/faq/#forgot-your-password'; + // const actions = { ...this.actions }; + const { alertMessage, authenticating, registering, registered } = state; + const act = registering ? i18n._('Create Account') : i18n._('Sign in'); + const docLink = 'http://www.makerverse.com/features/security/'; + let enabled = !authenticating && this.fields.username && this.fields.username.value.length > 0 && + this.fields.password && this.fields.password.value.length > 0; + if (enabled && registering) { + enabled = state.errors.passwordMatch.length === 0; + } if (state.redirectToReferrer) { const query = qs.parse(window.location.search, { ignoreQueryPrefix: true }); @@ -103,77 +240,176 @@ class Login extends PureComponent { } log.debug(`Redirect from "/login" to "${from.pathname}"`); - return ( ); } + if (registered) { + return ( +
+
+
+ Confirm your Email Address +
+
+ {'Please check your inbox for an email from:'} +
+ hello@openwork.shop +
+
+ {this.renderResend()} +
+
+ +
+
+ ); + } + return (
- {alertMessage && ( - -
{i18n._('Error')}
-
{alertMessage}
-
- )}
-
- -
- {i18n._('Sign in to {{name}}', { name: settings.productName })} + + {act}
-
-
- { - this.fields.name = node; - }} - type="text" - className="form-control" - placeholder={i18n._('Username')} - /> +
+ -
- { - this.fields.password = node; - }} - type="password" - className="form-control" - placeholder={i18n._('Password')} - /> -
-
- -
-

- - {i18n._('Forgot your password?')} - -

- + {this.renderError('username')} +
+
+ { + this.fields.password = node; + }} + type="password" + onChange={(e) => this.onChangePasswords(e.target.value)} + className="form-control" + placeholder={i18n._('Password')} + autoComplete="password" + /> + {this.renderError('password')} +
+ {registering && ( +
+ { + this.fields.passwordMatch = node; + }} + type="password" + className="form-control" + onChange={() => this.onChangePasswords()} + placeholder={i18n._('Password (again)')} + autoComplete="password" + /> + {this.renderError('passwordMatch')} +
+ )} + {!registering && ( + + )} +
+ {this.renderError('unknown')} + + {alertMessage && ( +
+ {alertMessage} +
+ )} +
+ +
+
+ + {i18n._('Why is it necessary to create an account?')} + +
); diff --git a/src/app/containers/Login/index.styl b/src/app/containers/Login/index.styl index 2e2fcd0fa..a58d2d343 100644 --- a/src/app/containers/Login/index.styl +++ b/src/app/containers/Login/index.styl @@ -1,14 +1,22 @@ @import "../variables"; .container { - background-color: #fff; + background-color: #f5f5f5; height: 100vh; + padding: 20px; } .login { - width: 300px; + width: 340px; margin: 0 auto; - padding-top: 40px; + background-color: #fff; + border-radius: 4px 4px 4px 4px; + -moz-border-radius: 4px 4px 4px 4px; + -webkit-border-radius: 4px 4px 4px 4px; + border: 1px solid #ccc; + -webkit-box-shadow: 0px 4px 4px 0px rgba(0,0,0,0.25); + -moz-box-shadow: 0px 4px 4px 0px rgba(0,0,0,0.25); + box-shadow: 0px 4px 4px 0px rgba(0,0,0,0.25); } .logo { @@ -19,13 +27,29 @@ } } +.error { + color: #a94442; + font-style: italic; + margin-top: 10px; +} + .title { - padding: 18px 0; + padding: 10px; font-size: 18px; line-height: 22px; text-align: center; + background-color: #f5f5f5; +} + +.content { + padding: 20px 20px 0px 20px; +} + +.footer { + background-color: #f5f5f5; + padding: 10px; + text-align: center; } .form { - margin-bottom: 15px; } diff --git a/src/app/i18n/cs/resource.json b/src/app/i18n/cs/resource.json index 4b5edaaae..eb7fc4cac 100644 --- a/src/app/i18n/cs/resource.json +++ b/src/app/i18n/cs/resource.json @@ -504,7 +504,6 @@ "Webcam Widget": "", "View a webcam from within the app to monitor progress.": "", "Baud Rate": "", - "Next Step": "", "Apply Calibration Results": "", "Calibrating": "", "Kinematics": "", @@ -528,7 +527,6 @@ "Move machine to 0/0": "", "Update Firmware": "", "Chains": "", - "Set Frame": "", "Stock": "", "Uplload Program": "", "Upload Program": "", @@ -539,5 +537,10 @@ "Export Calibration": "", "Import": "", "yes": "", - "no": "" + "no": "", + "Need help updating?": "", + "Program: Start": "", + "Program: Pause": "", + "Program: Stop": "", + "Program: Resume": "" } diff --git a/src/app/i18n/de/resource.json b/src/app/i18n/de/resource.json index a4afef57c..75a3a1573 100644 --- a/src/app/i18n/de/resource.json +++ b/src/app/i18n/de/resource.json @@ -504,7 +504,6 @@ "Webcam Widget": "", "View a webcam from within the app to monitor progress.": "", "Baud Rate": "", - "Next Step": "", "Apply Calibration Results": "", "Calibrating": "", "Kinematics": "", @@ -528,7 +527,6 @@ "Move machine to 0/0": "", "Update Firmware": "", "Chains": "", - "Set Frame": "", "Stock": "", "Uplload Program": "", "Upload Program": "", @@ -539,5 +537,10 @@ "Export Calibration": "", "Import": "", "yes": "", - "no": "" + "no": "", + "Need help updating?": "", + "Program: Start": "", + "Program: Pause": "", + "Program: Stop": "", + "Program: Resume": "" } diff --git a/src/app/i18n/en/resource.json b/src/app/i18n/en/resource.json index 0c3efff28..7ee0f1e3f 100644 --- a/src/app/i18n/en/resource.json +++ b/src/app/i18n/en/resource.json @@ -504,7 +504,6 @@ "Webcam Widget": "Webcam Widget", "View a webcam from within the app to monitor progress.": "View a webcam from within the app to monitor progress.", "Baud Rate": "Baud Rate", - "Next Step": "Next Step", "Apply Calibration Results": "Apply Calibration Results", "Calibrating": "Calibrating", "Kinematics": "Kinematics", @@ -528,7 +527,6 @@ "Move machine to 0/0": "Move machine to 0/0", "Update Firmware": "Update Firmware", "Chains": "Chains", - "Set Frame": "Set Frame", "Stock": "Stock", "Uplload Program": "Uplload Program", "Upload Program": "Upload Program", @@ -539,5 +537,10 @@ "Export Calibration": "Export Calibration", "Import": "Import", "yes": "yes", - "no": "no" + "no": "no", + "Need help updating?": "Need help updating?", + "Program: Start": "Program: Start", + "Program: Pause": "Program: Pause", + "Program: Stop": "Program: Stop", + "Program: Resume": "Program: Resume" } diff --git a/src/app/i18n/es/resource.json b/src/app/i18n/es/resource.json index dceb91688..180a37f55 100644 --- a/src/app/i18n/es/resource.json +++ b/src/app/i18n/es/resource.json @@ -504,7 +504,6 @@ "Webcam Widget": "", "View a webcam from within the app to monitor progress.": "", "Baud Rate": "", - "Next Step": "", "Apply Calibration Results": "", "Calibrating": "", "Kinematics": "", @@ -528,7 +527,6 @@ "Move machine to 0/0": "", "Update Firmware": "", "Chains": "", - "Set Frame": "", "Stock": "", "Uplload Program": "", "Upload Program": "", @@ -539,5 +537,10 @@ "Export Calibration": "", "Import": "", "yes": "", - "no": "" + "no": "", + "Need help updating?": "", + "Program: Start": "", + "Program: Pause": "", + "Program: Stop": "", + "Program: Resume": "" } diff --git a/src/app/i18n/fr/resource.json b/src/app/i18n/fr/resource.json index 897756366..69ccdf40d 100644 --- a/src/app/i18n/fr/resource.json +++ b/src/app/i18n/fr/resource.json @@ -504,7 +504,6 @@ "Webcam Widget": "", "View a webcam from within the app to monitor progress.": "", "Baud Rate": "", - "Next Step": "", "Apply Calibration Results": "", "Calibrating": "", "Kinematics": "", @@ -528,7 +527,6 @@ "Move machine to 0/0": "", "Update Firmware": "", "Chains": "", - "Set Frame": "", "Stock": "", "Uplload Program": "", "Upload Program": "", @@ -539,5 +537,10 @@ "Export Calibration": "", "Import": "", "yes": "", - "no": "" + "no": "", + "Need help updating?": "", + "Program: Start": "", + "Program: Pause": "", + "Program: Stop": "", + "Program: Resume": "" } diff --git a/src/app/i18n/hu/resource.json b/src/app/i18n/hu/resource.json index e8fc62939..250d3eb49 100644 --- a/src/app/i18n/hu/resource.json +++ b/src/app/i18n/hu/resource.json @@ -504,7 +504,6 @@ "Webcam Widget": "", "View a webcam from within the app to monitor progress.": "", "Baud Rate": "", - "Next Step": "", "Apply Calibration Results": "", "Calibrating": "", "Kinematics": "", @@ -528,7 +527,6 @@ "Move machine to 0/0": "", "Update Firmware": "", "Chains": "", - "Set Frame": "", "Stock": "", "Uplload Program": "", "Upload Program": "", @@ -539,5 +537,10 @@ "Export Calibration": "", "Import": "", "yes": "", - "no": "" + "no": "", + "Need help updating?": "", + "Program: Start": "", + "Program: Pause": "", + "Program: Stop": "", + "Program: Resume": "" } diff --git a/src/app/i18n/it/resource.json b/src/app/i18n/it/resource.json index 8a19e4ebd..87b1d79c6 100644 --- a/src/app/i18n/it/resource.json +++ b/src/app/i18n/it/resource.json @@ -504,7 +504,6 @@ "Webcam Widget": "", "View a webcam from within the app to monitor progress.": "", "Baud Rate": "", - "Next Step": "", "Apply Calibration Results": "", "Calibrating": "", "Kinematics": "", @@ -528,7 +527,6 @@ "Move machine to 0/0": "", "Update Firmware": "", "Chains": "", - "Set Frame": "", "Stock": "", "Uplload Program": "", "Upload Program": "", @@ -539,5 +537,10 @@ "Export Calibration": "", "Import": "", "yes": "", - "no": "" + "no": "", + "Need help updating?": "", + "Program: Start": "", + "Program: Pause": "", + "Program: Stop": "", + "Program: Resume": "" } diff --git a/src/app/i18n/ja/resource.json b/src/app/i18n/ja/resource.json index deb4cb6cb..cb3711942 100644 --- a/src/app/i18n/ja/resource.json +++ b/src/app/i18n/ja/resource.json @@ -504,7 +504,6 @@ "Webcam Widget": "", "View a webcam from within the app to monitor progress.": "", "Baud Rate": "", - "Next Step": "", "Apply Calibration Results": "", "Calibrating": "", "Kinematics": "", @@ -528,7 +527,6 @@ "Move machine to 0/0": "", "Update Firmware": "", "Chains": "", - "Set Frame": "", "Stock": "", "Uplload Program": "", "Upload Program": "", @@ -539,5 +537,10 @@ "Export Calibration": "", "Import": "", "yes": "", - "no": "" + "no": "", + "Need help updating?": "", + "Program: Start": "", + "Program: Pause": "", + "Program: Stop": "", + "Program: Resume": "" } diff --git a/src/app/i18n/nl/resource.json b/src/app/i18n/nl/resource.json index 759f65cdb..acd463ff6 100644 --- a/src/app/i18n/nl/resource.json +++ b/src/app/i18n/nl/resource.json @@ -504,7 +504,6 @@ "Webcam Widget": "", "View a webcam from within the app to monitor progress.": "", "Baud Rate": "", - "Next Step": "", "Apply Calibration Results": "", "Calibrating": "", "Kinematics": "", @@ -528,7 +527,6 @@ "Move machine to 0/0": "", "Update Firmware": "", "Chains": "", - "Set Frame": "", "Stock": "", "Uplload Program": "", "Upload Program": "", @@ -539,5 +537,10 @@ "Export Calibration": "", "Import": "", "yes": "", - "no": "" + "no": "", + "Need help updating?": "", + "Program: Start": "", + "Program: Pause": "", + "Program: Stop": "", + "Program: Resume": "" } diff --git a/src/app/i18n/pt-br/resource.json b/src/app/i18n/pt-br/resource.json index 97c7a8678..412d25d80 100644 --- a/src/app/i18n/pt-br/resource.json +++ b/src/app/i18n/pt-br/resource.json @@ -504,7 +504,6 @@ "Webcam Widget": "", "View a webcam from within the app to monitor progress.": "", "Baud Rate": "", - "Next Step": "", "Apply Calibration Results": "", "Calibrating": "", "Kinematics": "", @@ -528,7 +527,6 @@ "Move machine to 0/0": "", "Update Firmware": "", "Chains": "", - "Set Frame": "", "Stock": "", "Uplload Program": "", "Upload Program": "", @@ -539,5 +537,10 @@ "Export Calibration": "", "Import": "", "yes": "", - "no": "" + "no": "", + "Need help updating?": "", + "Program: Start": "", + "Program: Pause": "", + "Program: Stop": "", + "Program: Resume": "" } diff --git a/src/app/i18n/ru/resource.json b/src/app/i18n/ru/resource.json index 7d099e1eb..4fa2ca09e 100644 --- a/src/app/i18n/ru/resource.json +++ b/src/app/i18n/ru/resource.json @@ -504,7 +504,6 @@ "Webcam Widget": "", "View a webcam from within the app to monitor progress.": "", "Baud Rate": "", - "Next Step": "", "Apply Calibration Results": "", "Calibrating": "", "Kinematics": "", @@ -528,7 +527,6 @@ "Move machine to 0/0": "", "Update Firmware": "", "Chains": "", - "Set Frame": "", "Stock": "", "Uplload Program": "", "Upload Program": "", @@ -539,5 +537,10 @@ "Export Calibration": "", "Import": "", "yes": "", - "no": "" + "no": "", + "Need help updating?": "", + "Program: Start": "", + "Program: Pause": "", + "Program: Stop": "", + "Program: Resume": "" } diff --git a/src/app/i18n/tr/resource.json b/src/app/i18n/tr/resource.json index b83cef0bd..0cdf17598 100644 --- a/src/app/i18n/tr/resource.json +++ b/src/app/i18n/tr/resource.json @@ -504,7 +504,6 @@ "Webcam Widget": "", "View a webcam from within the app to monitor progress.": "", "Baud Rate": "", - "Next Step": "", "Apply Calibration Results": "", "Calibrating": "", "Kinematics": "", @@ -528,7 +527,6 @@ "Move machine to 0/0": "", "Update Firmware": "", "Chains": "", - "Set Frame": "", "Stock": "", "Uplload Program": "", "Upload Program": "", @@ -539,5 +537,10 @@ "Export Calibration": "", "Import": "", "yes": "", - "no": "" + "no": "", + "Need help updating?": "", + "Program: Start": "", + "Program: Pause": "", + "Program: Stop": "", + "Program: Resume": "" } diff --git a/src/app/i18n/zh-cn/resource.json b/src/app/i18n/zh-cn/resource.json index efafcbc58..b9759f3e5 100644 --- a/src/app/i18n/zh-cn/resource.json +++ b/src/app/i18n/zh-cn/resource.json @@ -504,7 +504,6 @@ "Webcam Widget": "", "View a webcam from within the app to monitor progress.": "", "Baud Rate": "", - "Next Step": "", "Apply Calibration Results": "", "Calibrating": "", "Kinematics": "", @@ -528,7 +527,6 @@ "Move machine to 0/0": "", "Update Firmware": "", "Chains": "", - "Set Frame": "", "Stock": "", "Uplload Program": "", "Upload Program": "", @@ -539,5 +537,10 @@ "Export Calibration": "", "Import": "", "yes": "", - "no": "" + "no": "", + "Need help updating?": "", + "Program: Start": "", + "Program: Pause": "", + "Program: Stop": "", + "Program: Resume": "" } diff --git a/src/app/i18n/zh-tw/resource.json b/src/app/i18n/zh-tw/resource.json index daf0edcd6..51c025df3 100644 --- a/src/app/i18n/zh-tw/resource.json +++ b/src/app/i18n/zh-tw/resource.json @@ -504,7 +504,6 @@ "Webcam Widget": "", "View a webcam from within the app to monitor progress.": "", "Baud Rate": "", - "Next Step": "", "Apply Calibration Results": "", "Calibrating": "", "Kinematics": "", @@ -528,7 +527,6 @@ "Move machine to 0/0": "", "Update Firmware": "", "Chains": "", - "Set Frame": "", "Stock": "", "Uplload Program": "", "Upload Program": "", @@ -539,5 +537,10 @@ "Export Calibration": "", "Import": "", "yes": "", - "no": "" + "no": "", + "Need help updating?": "", + "Program: Start": "", + "Program: Pause": "", + "Program: Stop": "", + "Program: Resume": "" } diff --git a/src/app/images/logo-ows-512.png b/src/app/images/logo-ows-512.png new file mode 100644 index 000000000..5dbb282b3 Binary files /dev/null and b/src/app/images/logo-ows-512.png differ diff --git a/src/app/index.jsx b/src/app/index.jsx index 9c6ca67c8..aa6709758 100644 --- a/src/app/index.jsx +++ b/src/app/index.jsx @@ -5,11 +5,14 @@ import pubsub from 'pubsub-js'; import qs from 'qs'; import React from 'react'; import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import { createBrowserHistory } from 'history'; import { HashRouter as Router, Route } from 'react-router-dom'; import i18next from 'i18next'; +import { OidcProvider } from 'redux-oidc'; import LanguageDetector from 'i18next-browser-languagedetector'; import XHR from 'i18next-xhr-backend'; import { TRACE, DEBUG, INFO, WARN, ERROR } from 'universal-logger'; @@ -22,8 +25,9 @@ import auth from './lib/auth'; import api from './api'; import series from './lib/promise-series'; import promisify from './lib/promisify'; -import * as user from './lib/user'; +import { authManager, signin, isAuthenticated } from './lib/user'; import store from './store'; +import configureReduxStore from './store/redux'; import App from './containers/App'; import Login from './containers/Login'; import Anchor from './components/Anchor'; @@ -37,24 +41,32 @@ import './styles/vendor.styl'; import './styles/app.styl'; const renderPage = () => { + const baseUrl = window.location.host; + const history = createBrowserHistory({ basename: baseUrl }); + const reduxStore = configureReduxStore(history); + const container = document.createElement('div'); document.body.appendChild(container); ReactDOM.render( - - -
- - -
-
-
, + + + + +
+ + +
+
+
+
+
, container ); }; @@ -94,7 +106,7 @@ series([ })(), () => promisify(next => { const token = store.get('session.token'); - user.signin({ token: token }) + signin({ token: token }) .then(({ authenticated, token }) => { if (authenticated) { log.debug('Authenticated'); @@ -108,6 +120,10 @@ series([ }); })(), () => promisify(next => { + if (!isAuthenticated()) { + next(); + return; + } api.workspaces .fetch() .then(({ body }) => { @@ -120,11 +136,10 @@ series([ log.error('workspaces load error'); } next(); - }); + }) + .then(Workspaces.connect); })() -]).then( - Workspaces.connect -).then(async () => { +]).then(async () => { log.info(`${settings.productName}`); // Cross-origin communication diff --git a/src/app/lib/user.js b/src/app/lib/user.js index 6202d7419..2ba5b4fdc 100644 --- a/src/app/lib/user.js +++ b/src/app/lib/user.js @@ -1,30 +1,74 @@ -import api from 'app/api'; +import { apirequest } from 'app/api'; import config from 'app/store'; +import { createUserManager } from 'redux-oidc'; +import log from 'app/lib/log'; -let _authenticated = false; +// Makerverse OAuth login mechanism. +// The oidc-client handles token hand-off and validation. -export const signin = ({ token, name, password }) => new Promise((resolve, reject) => { - api.signin({ token, name, password }) - .then((res) => { - const { enabled = false, token = '', name = '' } = { ...res.body }; +const devServer = true; +const authority = devServer ? 'http://localhost:5000' : 'https://openwork.shop'; +const self = 'http://localhost:8000'; +const oauthConfig = { + client_id: 'Makerverse', + redirect_uri: `${self}/#/account/logged-in`, + post_logout_redirect_uri: `${self}/#/account/logged-out`, + response_type: 'code', + scope: 'OpenWorkShopAPI openid profile', + authority: `${authority}/`, + silent_redirect_uri: `${self}/silent_renew.html`, + automaticSilentRenew: true, + filterProtocolClaims: true, + loadUserInfo: true, + monitorSession: false, +}; +export const authManager = createUserManager(oauthConfig); - config.set('session.enabled', enabled); - config.set('session.token', token); - config.set('session.name', name); +export const userProfile = { + user: null, +}; - // Persist data after successful login to prevent debounced update - config.persist(); +let _authenticated = false; - _authenticated = true; - resolve({ authenticated: true, token: token }); +export const getReturnUrl = () => new Promise((resolve, reject) => { + authManager + .createSigninRequest() + .then((p) => { + resolve(p.url); + }); + // resolve(`${self}/logged-in`); +}); + +export const call = (path, args) => new Promise((resolve, reject) => { + getReturnUrl() + .then((url) => { + args.returnUrl = url; + log.debug(path, 'request', url); + return apirequest.post(`${authority}/api/auth/${path}`).send(args); // withCredentials(). + }) + .then((res) => { + log.debug(path, 'response: ', res.body); + if (!res.body.record) { + throw new Error('Response was missing data'); + } else if (res.body.meta && res.body.meta.redirectUrl) { + window.location.replace(res.body.meta.redirectUrl); + } else { + resolve({ success: true }); + } }) - .catch((res) => { - // Do not unset session token so it won't trigger an update to the store - _authenticated = false; - resolve({ authenticated: false, token: null }); + .catch((err) => { + let body = err.response ? err.response.body : {}; + log.error(path, 'error:', err.message, body); + resolve({ success: false, errors: body.errors || [err.message] }); }); }); +export const signin = (args) => call('login', args); + +export const signup = (args) => call('register', args); + +export const resend = (args) => call('send/email', args); + export const signout = () => new Promise((resolve, reject) => { config.unset('session.token'); _authenticated = false; diff --git a/src/app/lib/workspaces.jsx b/src/app/lib/workspaces.jsx index ea757059a..a7f85e27c 100644 --- a/src/app/lib/workspaces.jsx +++ b/src/app/lib/workspaces.jsx @@ -13,6 +13,7 @@ import store from '../store'; import analytics from './analytics'; import Hardware from './hardware'; import ActiveState from './active-state'; +import isAuthenticated from './user'; import { MASLOW, GRBL, @@ -56,7 +57,7 @@ class Workspaces extends events.EventEmitter { } static connect() { - const funcs = Object.keys(Workspaces.all).map((id) => { + const funcs = isAuthenticated() ? Object.keys(Workspaces.all).map((id) => { return () => promisify(next => { const workspace = Workspaces.all[id]; workspace.controller.connect(auth.host, auth.options, () => { @@ -64,7 +65,7 @@ class Workspaces extends events.EventEmitter { next(); }); })(); - }); + }) : []; return series(funcs); } diff --git a/src/app/store/redux.js b/src/app/store/redux.js new file mode 100644 index 000000000..b46907be7 --- /dev/null +++ b/src/app/store/redux.js @@ -0,0 +1,32 @@ +import { applyMiddleware, combineReducers, compose, createStore } from 'redux'; +import * as Oidc from 'redux-oidc'; +import { authManager } from '../lib/user'; + +const reducers = { +}; + +// This is pretty ugly. The CNCjs app had a concept of a "store," then Redux was added. +// The two should be unufied. +export default function configureStore(history, initialState) { + // Oidc.Log.logger = console; + // Oidc.Log.level = Oidc.Log.DEBUG; + + const middleware = []; + + const rootReducer = combineReducers({ + ...reducers, + oidc: Oidc.reducer, + }); + + const enhancers = []; + + const store = createStore( + rootReducer, + initialState, + compose(applyMiddleware(...middleware), ...enhancers) + ); + + Oidc.loadUser(store, authManager); + + return store; +} diff --git a/src/app/widgets/Visualizer/Visualizer.jsx b/src/app/widgets/Visualizer/Visualizer.jsx index edad66076..41c50f5c6 100644 --- a/src/app/widgets/Visualizer/Visualizer.jsx +++ b/src/app/widgets/Visualizer/Visualizer.jsx @@ -430,7 +430,12 @@ class Visualizer extends Component { gapSize, scale, }; - return new Cuboid(cb); + const cuboid = new Cuboid(cb); + cuboid.position.set( + this.workspace.axes.x.middle, + this.workspace.axes.y.middle, + this.workspace.axes.z.middle); + return cuboid; } _axisLabels = []; @@ -684,7 +689,6 @@ class Visualizer extends Component { this.limits.name = 'Limits'; this.limits.visible = objects.limits.visible; this.group.add(this.limits); - this.limits.position.set(0, 0, 0); // always extend from machine origin. } this.scene.add(this.group);