diff --git a/embark-ui/package.json b/embark-ui/package.json
index 7ef7a960..f145e7c6 100644
--- a/embark-ui/package.json
+++ b/embark-ui/package.json
@@ -11,6 +11,7 @@
"connected-react-router": "^4.3.0",
"history": "^4.7.2",
"prop-types": "^15.6.2",
+ "query-string": "^6.1.0",
"react": "^16.4.2",
"react-ace": "^6.1.4",
"react-copy-to-clipboard": "^5.0.1",
diff --git a/embark-ui/src/actions/index.js b/embark-ui/src/actions/index.js
index 1e6d936a..967c1026 100644
--- a/embark-ui/src/actions/index.js
+++ b/embark-ui/src/actions/index.js
@@ -13,6 +13,13 @@ function action(type, payload = {}) {
return {type, ...payload};
}
+export const AUTHORIZE = createRequestTypes('AUTHORIZE');
+export const authorize = {
+ request: (token, callback) => action(AUTHORIZE[REQUEST], {token, callback}),
+ success: () => action(AUTHORIZE[SUCCESS]),
+ failure: (error) => action(AUTHORIZE[FAILURE], {error})
+};
+
export const ACCOUNTS = createRequestTypes('ACCOUNTS');
export const accounts = {
request: () => action(ACCOUNTS[REQUEST]),
@@ -219,6 +226,20 @@ export const saveCurrentFile = {
failure: () => action(SAVE_CURRENT_FILE[FAILURE])
};
+export const GET_TOKEN = createRequestTypes('TOKEN');
+export const getToken = {
+ request: (callback) => action(GET_TOKEN[REQUEST], {callback}),
+ success: (token) => action(GET_TOKEN[SUCCESS], {token}),
+ failure: () => action(GET_TOKEN[FAILURE])
+};
+
+export const POST_TOKEN = createRequestTypes('POST_TOKEN');
+export const postToken = {
+ request: (token) => action(POST_TOKEN[REQUEST], {token}),
+ success: (token) => action(POST_TOKEN[SUCCESS], {token}),
+ failure: () => action(POST_TOKEN[FAILURE])
+};
+
export const GAS_ORACLE = createRequestTypes('GAS_ORACLE');
export const gasOracle = {
request: () => action(GAS_ORACLE[REQUEST]),
diff --git a/embark-ui/src/components/AuthError.js b/embark-ui/src/components/AuthError.js
new file mode 100644
index 00000000..38eaccce
--- /dev/null
+++ b/embark-ui/src/components/AuthError.js
@@ -0,0 +1,24 @@
+import PropTypes from "prop-types";
+import React from 'react';
+import {Page, Alert, Form, Button} from "tabler-react";
+
+const AuthError = ({error}) => {
+ return
+
+ {error}
+
+
+
+
+ ;
+};
+
+AuthError.propTypes = {
+ error: PropTypes.string.isRequired
+};
+
+export default AuthError;
+
diff --git a/embark-ui/src/containers/AppContainer.js b/embark-ui/src/containers/AppContainer.js
index 45312588..3480d532 100644
--- a/embark-ui/src/containers/AppContainer.js
+++ b/embark-ui/src/containers/AppContainer.js
@@ -1,20 +1,48 @@
-import {ConnectedRouter} from "connected-react-router";
import PropTypes from "prop-types";
import {connect} from 'react-redux';
import React, {Component} from 'react';
+import {withRouter} from "react-router-dom";
-import history from '../history';
-import Layout from '../components/Layout';
import routes from '../routes';
+import AuthError from '../components/AuthError';
+import queryString from 'query-string';
import {
initBlockHeader,
+ authorize, getToken, postToken,
processes as processesAction,
versions as versionsAction,
plugins as pluginsAction
} from '../actions';
class AppContainer extends Component {
+ constructor (props) {
+ super(props);
+ this.state = {
+ authenticateError: null
+ };
+
+ this.checkToken();
+ }
+
+ checkToken() {
+ if (this.props.location.search) {
+ const token = queryString.parse(this.props.location.search).token;
+ this.props.postToken(token);
+ return this.props.authorize(token, this.authCallback.bind(this));
+ }
+ this.props.getToken((err, token) => {
+ this.props.authorize(token, this.authCallback.bind(this));
+ });
+ }
+
+ authCallback(err) {
+ if (err) {
+ return this.setState({authenticateError: err});
+ }
+ this.setState({authenticateError: null});
+ }
+
componentDidMount() {
this.props.initBlockHeader();
this.props.fetchProcesses();
@@ -23,30 +51,33 @@ class AppContainer extends Component {
}
render() {
- return (
-
-
- {routes}
-
-
- );
+ if (this.state.authenticateError) {
+ return ;
+ }
+ return ({routes});
}
}
AppContainer.propTypes = {
+ authorize: PropTypes.func,
+ getToken: PropTypes.func,
+ postToken: PropTypes.func,
initBlockHeader: PropTypes.func,
fetchProcesses: PropTypes.func,
fetchPlugins: PropTypes.func,
- fetchVersions: PropTypes.func
+ fetchVersions: PropTypes.func,
+ location: PropTypes.object
};
-export default connect(
+export default withRouter(connect(
null,
{
initBlockHeader,
+ authorize: authorize.request,
+ getToken: getToken.request,
+ postToken: postToken.request,
fetchProcesses: processesAction.request,
-
fetchVersions: versionsAction.request,
fetchPlugins: pluginsAction.request
},
-)(AppContainer);
+)(AppContainer));
diff --git a/embark-ui/src/index.js b/embark-ui/src/index.js
index 2624fd8b..7f69eded 100644
--- a/embark-ui/src/index.js
+++ b/embark-ui/src/index.js
@@ -1,3 +1,4 @@
+import {ConnectedRouter} from "connected-react-router";
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
@@ -5,8 +6,10 @@ import {Provider} from 'react-redux';
import "tabler-react/dist/Tabler.css";
import "./general.css";
import "./slider.css";
+import Layout from "./components/Layout";
import AppContainer from './containers/AppContainer';
+import history from "./history";
import registerServiceWorker from './registerServiceWorker';
import configureStore from './store/configureStore';
@@ -14,7 +17,11 @@ const store = configureStore();
ReactDOM.render(
-
+
+
+
+
+
,
document.getElementById('root')
);
diff --git a/embark-ui/src/reducers/index.js b/embark-ui/src/reducers/index.js
index 449fa139..af2ab2b1 100644
--- a/embark-ui/src/reducers/index.js
+++ b/embark-ui/src/reducers/index.js
@@ -73,7 +73,7 @@ const filtrer = {
},
ensRecords: function(record, index, self) {
return record.name && record.address && record.address !== voidAddress && index === self.findIndex((r) => (
- r.address=== record.address && r.name === record.name
+ r.address === record.address && r.name === record.name
));
},
files: function(file, index, self) {
@@ -137,7 +137,7 @@ function loading(_state = false, action) {
}
function compilingContract(state = false, action) {
- if(action.type === CONTRACT_COMPILE[REQUEST]) {
+ if (action.type === CONTRACT_COMPILE[REQUEST]) {
return true;
} else if (action.type === CONTRACT_COMPILE[FAILURE] || action.type === CONTRACT_COMPILE[SUCCESS]) {
return false;
@@ -146,12 +146,17 @@ function compilingContract(state = false, action) {
return state;
}
+function token(state = null, action) {
+ return (action.token) ? action.token : state;
+}
+
const rootReducer = combineReducers({
entities,
loading,
compilingContract,
errorMessage,
- errorEntities
+ errorEntities,
+ token
});
export default rootReducer;
diff --git a/embark-ui/src/sagas/index.js b/embark-ui/src/sagas/index.js
index 07eaab57..f311a0e1 100644
--- a/embark-ui/src/sagas/index.js
+++ b/embark-ui/src/sagas/index.js
@@ -2,9 +2,12 @@ import * as actions from '../actions';
import * as api from '../services/api';
import * as storage from '../services/storage';
import {eventChannel} from 'redux-saga';
-import {all, call, fork, put, takeEvery, take} from 'redux-saga/effects';
+import {all, call, fork, put, takeEvery, take, select} from 'redux-saga/effects';
function *doRequest(entity, serviceFn, payload) {
+ payload.token = yield select(function (state) {
+ return state.token;
+ });
const {response, error} = yield call(serviceFn, payload);
if(response) {
yield put(entity.success(response.data, payload));
@@ -39,10 +42,13 @@ export const fetchFile = doRequest.bind(null, actions.file, api.fetchFile);
export const postFile = doRequest.bind(null, actions.saveFile, api.postFile);
export const deleteFile = doRequest.bind(null, actions.removeFile, api.deleteFile);
export const fetchEthGas = doRequest.bind(null, actions.gasOracle, api.getEthGasAPI);
+export const authorize = doRequest.bind(null, actions.authorize, api.authorize);
export const fetchCurrentFile = doRequest.bind(null, actions.currentFile, storage.fetchCurrentFile);
export const postCurrentFile = doRequest.bind(null, actions.saveCurrentFile, storage.postCurrentFile);
export const deleteCurrentFile = doRequest.bind(null, null, storage.deleteCurrentFile);
+export const fetchToken = doRequest.bind(null, actions.getToken, storage.fetchToken);
+export const postToken = doRequest.bind(null, actions.postToken, storage.postToken);
export function *watchFetchTransaction() {
@@ -166,10 +172,22 @@ export function *watchPostCurrentFile() {
yield takeEvery(actions.SAVE_CURRENT_FILE[actions.REQUEST], postCurrentFile);
}
+export function *watchFetchToken() {
+ yield takeEvery(actions.GET_TOKEN[actions.REQUEST], fetchToken);
+}
+
+export function *watchPostToken() {
+ yield takeEvery(actions.POST_TOKEN[actions.REQUEST], postToken);
+}
+
export function *watchFetchEthGas() {
yield takeEvery(actions.GAS_ORACLE[actions.REQUEST], fetchEthGas);
}
+export function *watchAuthenticate() {
+ yield takeEvery(actions.AUTHORIZE[actions.REQUEST], authorize);
+}
+
function createChannel(socket) {
return eventChannel(emit => {
socket.onmessage = ((message) => {
@@ -278,7 +296,10 @@ export default function *root() {
fork(watchFetchFileSuccess),
fork(watchFetchCurrentFile),
fork(watchPostCurrentFile),
+ fork(watchFetchToken),
+ fork(watchPostToken),
fork(watchFetchEthGas),
+ fork(watchAuthenticate),
fork(watchListenGasOracle)
]);
}
diff --git a/embark-ui/src/services/api.js b/embark-ui/src/services/api.js
index fd0d5944..90ec8681 100644
--- a/embark-ui/src/services/api.js
+++ b/embark-ui/src/services/api.js
@@ -1,143 +1,147 @@
import axios from "axios";
import constants from '../constants';
-function get(path, params, endpoint) {
- return axios.get((endpoint || constants.httpEndpoint) + path, params)
+function request(type, path, params = {}, endpoint) {
+ axios.defaults.headers.common['Authorization'] = params.token;
+ const callback = params.callback || function() {};
+ return axios[type]((endpoint || constants.httpEndpoint) + path, params)
.then((response) => {
- return {response, error: null};
+ const data = (response.data && response.data.error) ? {error: response.data.error} : {response, error: null};
+ callback(data.error, data.response);
+ return data;
}).catch((error) => {
- return {response: null, error: error.message || 'Something bad happened'};
+ const data = {response: null, error: error.message || 'Something bad happened'};
+ callback(data.error, data.response);
+ return data;
});
}
-function post(path, params) {
- return axios.post(constants.httpEndpoint + path, params)
- .then((response) => {
- return {response, error: null};
- })
- .catch((error) => {
- return {response: null, error: error.message || 'Something bad happened'};
- });
+function get() {
+ return request('get', ...arguments);
}
-function destroy(path, params) {
- return axios.delete(constants.httpEndpoint + path, params)
- .then((response) => {
- return {response, error: null};
- })
- .catch((error) => {
- return {response: null, error: error.message || 'Something bad happened'};
- });
+function post() {
+ return request('post', ...arguments);
}
-export function postCommand(payload) {
- return post('/command', payload);
+function destroy() {
+ return request('delete', ...arguments);
+}
+
+export function postCommand() {
+ return post('/command', ...arguments);
}
export function fetchAccounts() {
- return get('/blockchain/accounts');
+ return get('/blockchain/accounts', ...arguments);
}
export function fetchAccount(payload) {
- return get(`/blockchain/accounts/${payload.address}`);
+ return get(`/blockchain/accounts/${payload.address}`, ...arguments);
}
export function fetchBlocks(payload) {
- return get('/blockchain/blocks', {params: payload});
+ return get('/blockchain/blocks', {params: payload, token: payload.token});
}
export function fetchBlock(payload) {
- return get(`/blockchain/blocks/${payload.blockNumber}`);
+ return get(`/blockchain/blocks/${payload.blockNumber}`, ...arguments);
}
export function fetchTransactions(payload) {
- return get('/blockchain/transactions', {params: payload});
+ return get('/blockchain/transactions', {params: payload, token: payload.token});
}
export function fetchTransaction(payload) {
- return get(`/blockchain/transactions/${payload.hash}`);
+ return get(`/blockchain/transactions/${payload.hash}`, ...arguments);
}
export function fetchProcesses() {
- return get('/processes');
+ return get('/processes', ...arguments);
}
export function fetchProcessLogs(payload) {
- return get(`/process-logs/${payload.processName}`);
+ return get(`/process-logs/${payload.processName}`, ...arguments);
}
export function fetchContractLogs() {
- return get(`/contracts/logs`);
+ return get(`/contracts/logs`, ...arguments);
}
export function fetchContracts() {
- return get('/contracts');
+ return get('/contracts', ...arguments);
}
export function fetchContract(payload) {
- return get(`/contract/${payload.contractName}`);
+ return get(`/contract/${payload.contractName}`, ...arguments);
}
export function postContractFunction(payload) {
- return post(`/contract/${payload.contractName}/function`, payload);
+ return post(`/contract/${payload.contractName}/function`, ...arguments);
}
export function postContractDeploy(payload) {
- return post(`/contract/${payload.contractName}/deploy`, payload);
+ return post(`/contract/${payload.contractName}/deploy`, ...arguments);
}
-export function postContractCompile(payload) {
- return post('/contract/compile', payload);
+export function postContractCompile() {
+ return post('/contract/compile', ...arguments);
}
export function fetchVersions() {
- return get('/versions');
+ return get('/versions', ...arguments);
}
export function fetchPlugins() {
- return get('/plugins');
+ return get('/plugins', ...arguments);
}
export function sendMessage(payload) {
- return post(`/communication/sendMessage`, payload.body);
+ return post(`/communication/sendMessage`, Object.assign({}, payload.body, {token: payload.token}));
}
export function fetchContractProfile(payload) {
- return get(`/profiler/${payload.contractName}`);
+ return get(`/profiler/${payload.contractName}`, ...arguments);
}
export function fetchEnsRecord(payload) {
+ const _payload = {params: payload, token: payload.token};
if (payload.name) {
- return get('/ens/resolve', {params: payload});
+ return get('/ens/resolve', _payload);
} else {
- return get('/ens/lookup', {params: payload});
+ return get('/ens/lookup', _payload);
}
}
-export function postEnsRecord(payload) {
- return post('/ens/register', payload);
+export function postEnsRecord() {
+ return post('/ens/register', ...arguments);
}
export function getEthGasAPI() {
- return get('/blockchain/gas/oracle', {});
+ return get('/blockchain/gas/oracle', ...arguments);
}
export function fetchFiles() {
- return get('/files');
+ return get('/files', ...arguments);
}
export function fetchFile(payload) {
- return get('/file', {params: payload});
+ return get('/file', {params: payload, token: payload.token});
}
-export function postFile(payload) {
- return post('/files', payload);
+export function postFile() {
+ return post('/files', ...arguments);
}
export function deleteFile(payload) {
- return destroy('/file', {params: payload});
+ return destroy('/file', {params: payload, token: payload.token});
}
+export function authorize() {
+ return post('/authorize', ...arguments);
+}
+
+// TODO token for WS?
export function listenToChannel(channel) {
return new WebSocket(`${constants.wsEndpoint}/communication/listenTo/${channel}`);
}
diff --git a/embark-ui/src/services/storage.js b/embark-ui/src/services/storage.js
index 8a8317ee..b9080681 100644
--- a/embark-ui/src/services/storage.js
+++ b/embark-ui/src/services/storage.js
@@ -17,3 +17,19 @@ export function deleteCurrentFile() {
resolve({});
});
}
+
+export function postToken(data) {
+ return new Promise(function(resolve) {
+ localStorage.setItem('token', data.token);
+ resolve({response: {data: data.token}});
+ });
+}
+
+export function fetchToken({callback}) {
+ callback = callback || function(){};
+ return new Promise(function(resolve) {
+ const token = localStorage.getItem('token');
+ callback(null, token);
+ resolve({response: {data: token}});
+ });
+}
diff --git a/lib/core/engine.js b/lib/core/engine.js
index 19e58793..824c79f2 100644
--- a/lib/core/engine.js
+++ b/lib/core/engine.js
@@ -202,6 +202,7 @@ class Engine {
webServerService(_options) {
this.registerModule('webserver', {plugins: this.plugins});
+ this.registerModule('authenticator');
}
storageService(_options) {
diff --git a/lib/modules/authenticator/index.js b/lib/modules/authenticator/index.js
new file mode 100644
index 00000000..8a6e6c8f
--- /dev/null
+++ b/lib/modules/authenticator/index.js
@@ -0,0 +1,57 @@
+const uuid = require('uuid/v1');
+
+const ERROR_OBJ = {error: __('Wrong authentication token. Get your token from the Embark console by typing `token`')};
+
+class Authenticator {
+ constructor(embark, _options) {
+ this.authToken = uuid();
+ this.embark = embark;
+ this.logger = embark.logger;
+ this.events = embark.events;
+
+ this.registerCalls();
+ this.registerEvents();
+ }
+
+ registerCalls() {
+ this.embark.registerAPICall(
+ 'post',
+ '/embark-api/authorize',
+ (req, res) => {
+ if (req.body.token !== this.authToken) {
+ this.logger.warn(__('Someone tried and failed to authorize to the backend'));
+ this.logger.warn(__('- User-Agent: %s', req.headers['user-agent']));
+ this.logger.warn(__('- Referer: %s', req.headers.referer));
+ return res.send(ERROR_OBJ);
+ }
+ res.send();
+ }
+ );
+
+ this.embark.registerConsoleCommand((cmd, _options) => {
+ return {
+ match: () => cmd === "token",
+ process: (callback) => {
+ callback(null, __('Your authorisation token: %s', this.authToken));
+ }
+ };
+ });
+ }
+
+ registerEvents() {
+ this.events.once('outputDone', () => {
+ const {port, host} = this.embark.config.webServerConfig;
+ this.logger.info(__('Access the web backend with the following url: %s',
+ (`http://${host}:${port}/embark?token=${this.authToken}`.underline)));
+ });
+
+ this.events.setCommandHandler('authenticator:authorize', (token, cb) => {
+ if (token !== this.authToken) {
+ return cb(ERROR_OBJ);
+ }
+ cb();
+ });
+ }
+}
+
+module.exports = Authenticator;
diff --git a/lib/modules/webserver/server.js b/lib/modules/webserver/server.js
index 90bad893..060aee2b 100644
--- a/lib/modules/webserver/server.js
+++ b/lib/modules/webserver/server.js
@@ -49,13 +49,13 @@ class Server {
for (let apiCall of apiCalls) {
console.dir("adding " + apiCall.method + " " + apiCall.endpoint);
- app[apiCall.method].apply(app, [apiCall.endpoint, apiCall.cb]);
+ app[apiCall.method].apply(app, [apiCall.endpoint, this.applyAPIFunction.bind(this, apiCall.cb)]);
}
}
this.events.on('plugins:register:api', (apiCall) => {
console.dir("adding " + apiCall.method + " " + apiCall.endpoint);
- app[apiCall.method].apply(app, [apiCall.endpoint, apiCall.cb]);
+ app[apiCall.method].apply(app, [apiCall.endpoint, this.applyAPIFunction.bind(this, apiCall.cb)]);
});
app.get('/embark/*', function(req, res) {
@@ -78,6 +78,16 @@ class Server {
":" + this.port).bold.underline.green);
}
+ applyAPIFunction (cb, req, res) {
+ this.events.request('authenticator:authorize', req.headers.authorization, (err) => {
+ if (err) {
+ const send = res.send ? res.send.bind(res) : req.send.bind(req); // WS only has the first params
+ return send(err);
+ }
+ cb(req, res);
+ });
+ }
+
stop(callback) {
callback = callback || function () {};
if (!this.server || !this.server.listening) {