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) {