From 2777e975c0199f03aa6727246b78b03a61cc32ce Mon Sep 17 00:00:00 2001 From: Zane Claes Date: Tue, 6 Oct 2020 10:18:58 -0600 Subject: [PATCH] Basic auth --- bin/launch | 5 +- docs/features/programs.md | 34 ++ docs/features/security.md | 22 + docs/installation/web-server/raspberry-pi.md | 7 + package.json | 11 +- src/app/api/index.js | 42 +- src/app/components/ProtectedRoute/index.jsx | 22 +- src/app/containers/Login/Login.jsx | 412 +++++++++++++++---- src/app/containers/Login/index.styl | 34 +- src/app/i18n/cs/resource.json | 9 +- src/app/i18n/de/resource.json | 9 +- src/app/i18n/en/resource.json | 9 +- src/app/i18n/es/resource.json | 9 +- src/app/i18n/fr/resource.json | 9 +- src/app/i18n/hu/resource.json | 9 +- src/app/i18n/it/resource.json | 9 +- src/app/i18n/ja/resource.json | 9 +- src/app/i18n/nl/resource.json | 9 +- src/app/i18n/pt-br/resource.json | 9 +- src/app/i18n/ru/resource.json | 9 +- src/app/i18n/tr/resource.json | 9 +- src/app/i18n/zh-cn/resource.json | 9 +- src/app/i18n/zh-tw/resource.json | 9 +- src/app/images/logo-ows-512.png | Bin 0 -> 30559 bytes src/app/index.jsx | 55 ++- src/app/lib/user.js | 78 +++- src/app/lib/workspaces.jsx | 5 +- src/app/store/redux.js | 32 ++ src/app/widgets/Visualizer/Visualizer.jsx | 8 +- 29 files changed, 694 insertions(+), 199 deletions(-) create mode 100644 docs/features/programs.md create mode 100644 docs/features/security.md create mode 100644 src/app/images/logo-ows-512.png create mode 100644 src/app/store/redux.js 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 ? '' : ( +
    {msgs.map((msg) => { + return
  • {msg}
  • ; + })} +
+ ); + } + + 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 0000000000000000000000000000000000000000..5dbb282b353b7d68c641f5fa0a1dd8f1ef0b365f GIT binary patch literal 30559 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 literal 0 HcmV?d00001 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);