From 7e082d939cfb8dc06af6b19205506a1c9cbc0bb2 Mon Sep 17 00:00:00 2001 From: bochaco Date: Fri, 9 Dec 2016 17:56:55 -0300 Subject: [PATCH 01/44] Getting the SD handle of an existing file directly from the FILE_INDEX array instead of querying the network. --- markdown_editor/src/store.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/markdown_editor/src/store.js b/markdown_editor/src/store.js index fbdb0c6..f08ea70 100644 --- a/markdown_editor/src/store.js +++ b/markdown_editor/src/store.js @@ -51,16 +51,7 @@ const _refreshConfig = () => { }; const getSDHandle = (filename) => { - let dataIdHandle = null; - return safeDataId.getStructuredDataHandle(ACCESS_TOKEN, btoa(`${USER_PREFIX}:${filename}`), 501) - .then(extractHandle) - .then(handleId => (dataIdHandle = handleId)) - .then(() => safeStructuredData.getHandle(ACCESS_TOKEN, dataIdHandle)) - .then(extractHandle) - .then(handleId => { - safeDataId.dropHandle(ACCESS_TOKEN, dataIdHandle); - return handleId; - }) + return Promise.resolve(FILE_INDEX[filename]); }; const updateFile = (filename, payload) => { From 76fabc8eb7733b2a1748fcb1183325e4ab5ffe15 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 10 Mar 2017 14:59:48 +0100 Subject: [PATCH 02/44] fix/Email Switch to latest electron forge setup --- email_app/.babelrc | 20 ----------- email_app/.compilerc | 30 ++++++++++++++++ email_app/app/app.html | 50 +++++++++++++++----------- email_app/app/app.js | 18 ++++++++++ email_app/app/index.js | 81 ++++++++++++++++++++++++++++++++---------- email_app/package.json | 62 ++++++++++++++++++++++++-------- 6 files changed, 189 insertions(+), 72 deletions(-) delete mode 100755 email_app/.babelrc create mode 100644 email_app/.compilerc create mode 100644 email_app/app/app.js mode change 100755 => 100644 email_app/package.json diff --git a/email_app/.babelrc b/email_app/.babelrc deleted file mode 100755 index f8d5ece..0000000 --- a/email_app/.babelrc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "presets": ["es2015", "stage-0", "react"], - "plugins": ["add-module-exports"], - "env": { - "production": { - "presets": ["react-optimize"], - "plugins": [ - "babel-plugin-dev-expression" - ] - }, - "development": { - "presets": ["react-hmre"] - }, - "test": { - "plugins": [ - ["webpack-loaders", { "config": "webpack.config.node.js", "verbose": false }] - ] - } - } -} diff --git a/email_app/.compilerc b/email_app/.compilerc new file mode 100644 index 0000000..d5cc5d8 --- /dev/null +++ b/email_app/.compilerc @@ -0,0 +1,30 @@ +{ +"application/javascript": { + "presets": [ + ["env", { "targets": { "electron": "1.6.0" } }], + "es2015", + "stage-0", + "react" + ], + "sourceMaps": "none", + "env": { + "development": { + "plugins": [ + "transform-async-to-generator", + "transform-class-properties", + "transform-es2015-classes", + "react-hot-loader/babel" + ], + "sourceMaps": "inline" + }, + "production": { + "plugins": [ + "transform-async-to-generator", + "transform-class-properties", + "transform-es2015-classes", + "react-optimize" + ] + } + } + } +} \ No newline at end of file diff --git a/email_app/app/app.html b/email_app/app/app.html index 22b1e53..899999d 100755 --- a/email_app/app/app.html +++ b/email_app/app/app.html @@ -1,21 +1,31 @@ - - - - SAFE Mail Tutorial - - - - - -
- - - + + + + + + SAFE Mail Tutorial + + + + + +
+ + + + \ No newline at end of file diff --git a/email_app/app/app.js b/email_app/app/app.js new file mode 100644 index 0000000..9305b50 --- /dev/null +++ b/email_app/app/app.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { render } from 'react-dom'; +import { Provider } from 'react-redux'; +import { Router, hashHistory } from 'react-router'; +import { syncHistoryWithStore } from 'react-router-redux'; +import routes from './routes'; +import configureStore from './store/configureStore'; + +const store = configureStore(); +const history = syncHistoryWithStore(hashHistory, store); + +export default class App extends React.Component { + render() { + return ( + + ); + } +} \ No newline at end of file diff --git a/email_app/app/index.js b/email_app/app/index.js index 4325352..0803d5b 100755 --- a/email_app/app/index.js +++ b/email_app/app/index.js @@ -1,18 +1,63 @@ -import React from 'react'; -import { render } from 'react-dom'; -import { Provider } from 'react-redux'; -import { Router, hashHistory } from 'react-router'; -import { syncHistoryWithStore } from 'react-router-redux'; -import routes from './routes'; -import configureStore from './store/configureStore'; -import './less/main.less'; - -const store = configureStore(); -const history = syncHistoryWithStore(hashHistory, store); - -render( - - - , - document.getElementById('root') -); +import { app, BrowserWindow } from 'electron'; +import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer'; +import { enableLiveReload } from 'electron-compile'; +require("babel-polyfill"); + + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow; + +const isDevMode = process.execPath.match(/[\\/]electron/); + +// if (isDevMode) enableLiveReload({strategy: 'react-hmr'}); + +const createWindow = async () => { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + }); + + // and load the index.html of the app. + mainWindow.loadURL(`file://${__dirname}/app.html`); + + // Open the DevTools. + if (isDevMode) { + await installExtension(REACT_DEVELOPER_TOOLS); + mainWindow.webContents.openDevTools(); + } + + // Emitted when the window is closed. + mainWindow.on('closed', () => { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = null; + }); +}; + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.on('ready', createWindow); + +// Quit when all windows are closed. +app.on('window-all-closed', () => { + // On OS X it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('activate', () => { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow(); + } +}); + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and import them here. diff --git a/email_app/package.json b/email_app/package.json old mode 100755 new mode 100644 index 2a79b65..5de3f86 --- a/email_app/package.json +++ b/email_app/package.json @@ -4,7 +4,7 @@ "version": "0.1.2", "description": "Mailing application tutorial using SAFE Network", "identifier": "net.maidsafe.mailtutorial", - "main": "main.js", + "main": "app/index.js", "scripts": { "test": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 mocha --compilers js:babel-register --recursive --require ./test/setup.js test/**/*.spec.js", "test-watch": "npm test -- --watch", @@ -14,12 +14,13 @@ "build-main": "cross-env NODE_ENV=production node -r babel-register ./node_modules/webpack/bin/webpack --config webpack.config.electron.js --progress --profile --colors", "build-renderer": "cross-env NODE_ENV=production node -r babel-register ./node_modules/webpack/bin/webpack --config webpack.config.production.js --progress --profile --colors", "build": "npm run build-main && npm run build-renderer", - "start": "cross-env NODE_ENV=production electron ./", + "start": "electron-forge start", "start-hot": "cross-env HOT=1 NODE_ENV=development electron -r babel-register -r babel-polyfill ./main.development", - "package": "cross-env NODE_ENV=production node -r babel-register -r babel-polyfill package.js", + "package": "electron-forge package", "package-all": "npm run package -- --all", "postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json", - "dev": "concurrently --kill-others \"npm run hot-server\" \"npm run start-hot\"" + "dev": "concurrently --kill-others \"npm run hot-server\" \"npm run start-hot\"", + "make": "electron-forge make" }, "bin": { "electron": "./node_modules/.bin/electron" @@ -48,24 +49,25 @@ "babel-loader": "^6.2.5", "babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-dev-expression": "^0.2.1", + "babel-plugin-transform-async-to-generator": "^6.22.0", "babel-plugin-webpack-loaders": "^0.7.1", "babel-polyfill": "^6.13.0", + "babel-preset-env": "^1.2.1", "babel-preset-es2015": "^6.14.0", - "babel-preset-react": "^6.11.1", + "babel-preset-react": "^6.23.0", "babel-preset-react-hmre": "^1.1.1", "babel-preset-react-optimize": "^1.0.1", "babel-preset-stage-0": "^6.5.0", "babel-register": "^6.14.0", - "classnames": "^2.2.5", "chai": "^3.5.0", + "classnames": "^2.2.5", "concurrently": "^2.2.0", "cross-env": "^2.0.0", "css-loader": "^0.24.0", "del": "^2.2.2", "devtron": "^1.3.0", - "electron": "^1.3.4", "electron-devtools-installer": "^2.0.1", - "electron-packager": "^7.7.0", + "electron-prebuilt-compile": "1.6.2", "electron-rebuild": "^1.2.0", "eslint": "^3.3.1", "eslint-config-airbnb": "^10.0.1", @@ -96,19 +98,22 @@ }, "dependencies": { "axios": "^0.14.0", + "babel-plugin-transform-class-properties": "^6.23.0", "buffer-stream-reader": "^0.1.1", "css-modules-require-hook": "^4.0.2", "dateformat": "^1.0.12", + "electron-compile": "^6.1.3", "electron-debug": "^1.0.1", "material-design-lite": "^1.2.1", "open-sans-fontface": "^1.4.0", "postcss": "^5.1.2", - "react": "^15.3.1", - "react-dom": "^15.3.1", - "react-redux": "^4.4.5", - "react-router": "^2.7.0", - "react-router-redux": "^4.0.5", - "redux": "^3.5.2", + "react": "^15.4.2", + "react-dom": "^15.4.2", + "react-hot-loader": "^3.0.0-beta.6", + "react-redux": "^5.0.3", + "react-router": "^3.0.2", + "react-router-redux": "^4.0.8", + "redux": "^3.6.0", "redux-axios-middleware": "^2.0.0", "redux-thunk": "^2.1.0", "source-map-support": "^0.4.2", @@ -117,5 +122,34 @@ "devEngines": { "node": "4.x || 5.x || 6.x", "npm": "2.x || 3.x" + }, + "config": { + "forge": { + "make_targets": { + "win32": [ + "squirrel" + ], + "darwin": [ + "zip" + ], + "linux": [ + "deb", + "rpm" + ] + }, + "electronPackagerConfig": {}, + "electronWinstallerConfig": { + "name": "" + }, + "electronInstallerDebian": {}, + "electronInstallerRedhat": {}, + "github_repository": { + "owner": "", + "name": "" + }, + "windowsStoreConfig": { + "packageName": "" + } + } } } From b0e43ea797d04036967ab77226e1ece6ef1ff9e9 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 10 Mar 2017 17:18:02 +0100 Subject: [PATCH 03/44] fix/Email remove cruft config files --- email_app/main.development.js | 196 ------------------------ email_app/package.js | 133 ---------------- email_app/webpack.config.base.js | 33 ---- email_app/webpack.config.development.js | 41 ----- email_app/webpack.config.electron.js | 38 ----- email_app/webpack.config.eslint.js | 3 - email_app/webpack.config.node.js | 12 -- email_app/webpack.config.production.js | 41 ----- 8 files changed, 497 deletions(-) delete mode 100755 email_app/main.development.js delete mode 100755 email_app/package.js delete mode 100755 email_app/webpack.config.base.js delete mode 100755 email_app/webpack.config.development.js delete mode 100755 email_app/webpack.config.electron.js delete mode 100755 email_app/webpack.config.eslint.js delete mode 100755 email_app/webpack.config.node.js delete mode 100755 email_app/webpack.config.production.js diff --git a/email_app/main.development.js b/email_app/main.development.js deleted file mode 100755 index baf4557..0000000 --- a/email_app/main.development.js +++ /dev/null @@ -1,196 +0,0 @@ -import { app, BrowserWindow, Menu, shell } from 'electron'; -import pkg from './package.json'; - -let menu; -let template; -let mainWindow = null; - - -if (process.env.NODE_ENV === 'development') { - require('electron-debug')(); // eslint-disable-line global-require -} - - -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') app.quit(); -}); - - -const installExtensions = async () => { - if (process.env.NODE_ENV === 'development') { - const installer = require('electron-devtools-installer'); // eslint-disable-line global-require - - const extensions = [ - 'REACT_DEVELOPER_TOOLS', - 'REDUX_DEVTOOLS' - ]; - const forceDownload = !!process.env.UPGRADE_EXTENSIONS; - for (const name of extensions) { - try { - await installer.default(installer[name], forceDownload); - } catch (e) {} // eslint-disable-line - } - } -}; - -app.on('ready', async () => { - await installExtensions(); - - mainWindow = new BrowserWindow({ - show: false, - resizable: false, - width: 1024, - height: 728 - }); - - mainWindow.loadURL(`file://${__dirname}/app/app.html`); - - mainWindow.webContents.on('did-finish-load', () => { - mainWindow.show(); - mainWindow.focus(); - }); - - mainWindow.on('closed', () => { - mainWindow = null; - }); - - if (process.env.NODE_ENV === 'development') { - mainWindow.openDevTools(); - mainWindow.webContents.on('context-menu', (e, props) => { - const { x, y } = props; - - Menu.buildFromTemplate([{ - label: 'Inspect element', - click() { - mainWindow.inspectElement(x, y); - } - }]).popup(mainWindow); - }); - } - - if (process.platform === 'darwin') { - template = [{ - label: `${pkg.productName}`, - submenu: [{ - label: `About ${pkg.productName}`, - selector: 'orderFrontStandardAboutPanel:' - }, { - type: 'separator' - }, { - label: 'Services', - submenu: [] - }, { - type: 'separator' - }, { - label: `Hide ${pkg.productName}`, - accelerator: 'Command+H', - selector: 'hide:' - }, { - label: 'Hide Others', - accelerator: 'Command+Shift+H', - selector: 'hideOtherApplications:' - }, { - label: 'Show All', - selector: 'unhideAllApplications:' - }, { - type: 'separator' - }, { - label: `Quit ${pkg.productName}`, - accelerator: 'Command+Q', - click() { - app.quit(); - } - }] - }, { - label: 'Edit', - submenu: [{ - label: 'Undo', - accelerator: 'Command+Z', - selector: 'undo:' - }, { - label: 'Redo', - accelerator: 'Shift+Command+Z', - selector: 'redo:' - }, { - type: 'separator' - }, { - label: 'Cut', - accelerator: 'Command+X', - selector: 'cut:' - }, { - label: 'Copy', - accelerator: 'Command+C', - selector: 'copy:' - }, { - label: 'Paste', - accelerator: 'Command+V', - selector: 'paste:' - }, { - label: 'Select All', - accelerator: 'Command+A', - selector: 'selectAll:' - }] - }, { - label: 'Window', - submenu: [{ - label: 'Minimize', - accelerator: 'Command+M', - selector: 'performMiniaturize:' - }, { - label: 'Close', - accelerator: 'Command+W', - selector: 'performClose:' - }, { - type: 'separator' - }, { - label: 'Bring All to Front', - selector: 'arrangeInFront:' - }] - }, { - label: 'Help', - submenu: [{ - label: 'Learn More', - click() { - shell.openExternal('https://maidsafe.readme.io'); - } - }, { - label: 'Community Discussions', - click() { - shell.openExternal('https://safenetforum.org/'); - } - }] - }]; - - menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(menu); - } else { - template = [{ - label: '&File', - submenu: [{ - label: '&Open', - accelerator: 'Ctrl+O' - }, { - label: '&Close', - accelerator: 'Ctrl+W', - click() { - mainWindow.close(); - } - }] - }, { - label: 'Help', - submenu: [{ - label: 'Learn More', - click() { - shell.openExternal('https://maidsafe.readme.io'); - } - }, { - label: 'Community Discussions', - click() { - shell.openExternal('https://safenetforum.org/'); - } - }] - }]; - menu = Menu.buildFromTemplate(template); - mainWindow.setMenu(menu); - } -}); diff --git a/email_app/package.js b/email_app/package.js deleted file mode 100755 index 4e3bfb8..0000000 --- a/email_app/package.js +++ /dev/null @@ -1,133 +0,0 @@ -/* eslint strict: 0, no-shadow: 0, no-unused-vars: 0, no-console: 0 */ -'use strict'; - -require('babel-polyfill'); -const os = require('os'); -const webpack = require('webpack'); -const electronCfg = require('./webpack.config.electron'); -const cfg = require('./webpack.config.production'); -const packager = require('electron-packager'); -const del = require('del'); -const exec = require('child_process').exec; -const argv = require('minimist')(process.argv.slice(2)); -const pkg = require('./package.json'); - -const deps = Object.keys(pkg.dependencies); -const devDeps = Object.keys(pkg.devDependencies); - -const appName = argv.name || argv.n || ((os.platform() === 'linux') ? pkg.name : pkg.productName); -const shouldUseAsar = argv.asar || argv.a || false; -const shouldBuildAll = argv.all || false; - -const DEFAULT_OPTS = { - dir: './', - name: appName, - asar: shouldUseAsar, - ignore: [ - '^/test($|/)', - '^/release($|/)', - '^/main.development.js' - ], - prune: true - /*.concat(devDeps.map(name => `/node_modules/${name}($|/)`)) - .concat( - deps.filter(name => !electronCfg.externals.includes(name)) - .map(name => `/node_modules/${name}($|/)`) - )*/ -}; - -const icon = argv.icon || argv.i || 'app/app'; - -if (icon) { - DEFAULT_OPTS.icon = icon; -} - -const version = argv.version || argv.v; - -if (version) { - DEFAULT_OPTS.version = version; - startPack(); -} else { - // use the same version as the currently-installed electron-prebuilt - exec('npm list electron --dev', (err, stdout) => { - if (err) { - DEFAULT_OPTS.version = '1.2.0'; - } else { - DEFAULT_OPTS.version = stdout.split('electron@')[1].replace(/\s/g, ''); - } - - startPack(); - }); -} - - -function build(cfg) { - return new Promise((resolve, reject) => { - webpack(cfg, (err, stats) => { - if (err) return reject(err); - resolve(stats); - }); - }); -} - -async function startPack() { - console.log('start pack...'); - - try { - await build(electronCfg); - await build(cfg); - const paths = await del('release'); - - if (shouldBuildAll) { - // build for all platforms - const archs = ['ia32', 'x64']; - const platforms = ['linux', 'win32', 'darwin']; - - platforms.forEach((plat) => { - archs.forEach((arch) => { - pack(plat, arch, log(plat, arch)); - }); - }); - } else { - // build for current platform only - pack(os.platform(), os.arch(), log(os.platform(), os.arch())); - } - } catch (error) { - console.error(error); - } -} - -function pack(plat, arch, cb) { - // there is no darwin ia32 electron - if (plat === 'darwin' && arch === 'ia32') return; - - const iconObj = { - icon: DEFAULT_OPTS.icon + (() => { - let extension = '.png'; - if (plat === 'darwin') { - extension = '.icns'; - } else if (plat === 'win32') { - extension = '.ico'; - } - return extension; - })() - }; - - const opts = Object.assign({}, DEFAULT_OPTS, iconObj, { - platform: plat, - arch, - prune: true, - 'app-version': pkg.version || DEFAULT_OPTS.version, - out: `release/${plat}-${arch}` - }); - - packager(opts, cb); -} - - -function log(plat, arch) { - return (err, filepath) => { - if (err) return console.error(err); - console.log(`${plat}-${arch} finished!`); - }; -} diff --git a/email_app/webpack.config.base.js b/email_app/webpack.config.base.js deleted file mode 100755 index 9851b17..0000000 --- a/email_app/webpack.config.base.js +++ /dev/null @@ -1,33 +0,0 @@ -import path from 'path'; - -export default { - module: { - loaders: [{ - test: /\.jsx?$/, - loaders: ['babel-loader'], - exclude: /node_modules/ - }, { - test: /\.json$/, - loader: 'json-loader' - }] - }, - output: { - path: path.join(__dirname, 'dist'), - filename: 'bundle.js', - libraryTarget: 'commonjs2' - }, - resolve: { - extensions: ['', '.js', '.jsx', '.json'], - packageMains: ['webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main'] - }, - plugins: [ - - ], - externals: { - 'font-awesome': 'font-awesome', - 'source-map-support': 'source-map-support', - 'open-sans-fontface': 'open-sans-fontface', - material: 'material', - 'material-icons': 'material-icons' - } -}; diff --git a/email_app/webpack.config.development.js b/email_app/webpack.config.development.js deleted file mode 100755 index 49859e4..0000000 --- a/email_app/webpack.config.development.js +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint max-len: 0 */ -import webpack from 'webpack'; -import merge from 'webpack-merge'; -import baseConfig from './webpack.config.base'; - -const port = process.env.PORT || 3000; - -export default merge(baseConfig, { - debug: true, - - devtool: 'cheap-module-eval-source-map', - - entry: [ - `webpack-hot-middleware/client?path=http://localhost:${port}/__webpack_hmr`, - './app/index' - ], - - output: { - publicPath: `http://localhost:${port}/dist/` - }, - - module: { - loaders: [ - { - test: /\.less$/, - loader: "style!css!less" - }, - { test: /\.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, loader: 'file-loader?name=fonts/[name].[ext]' } - ] - }, - - plugins: [ - new webpack.HotModuleReplacementPlugin(), - new webpack.NoErrorsPlugin(), - new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify('development') - }) - ], - - target: 'electron-renderer' -}); diff --git a/email_app/webpack.config.electron.js b/email_app/webpack.config.electron.js deleted file mode 100755 index 7f32f6e..0000000 --- a/email_app/webpack.config.electron.js +++ /dev/null @@ -1,38 +0,0 @@ -import webpack from 'webpack'; -import merge from 'webpack-merge'; -import baseConfig from './webpack.config.base'; - -export default merge(baseConfig, { - devtool: 'source-map', - - entry: ['babel-polyfill', './main.development'], - - output: { - path: __dirname, - filename: './main.js' - }, - - plugins: [ - new webpack.optimize.UglifyJsPlugin({ - compressor: { - warnings: false - } - }), - new webpack.BannerPlugin( - 'require("source-map-support").install();', - { raw: true, entryOnly: false } - ), - new webpack.DefinePlugin({ - 'process.env': { - NODE_ENV: JSON.stringify('production') - } - }) - ], - - target: 'electron-main', - - node: { - __dirname: false, - __filename: false - } -}); diff --git a/email_app/webpack.config.eslint.js b/email_app/webpack.config.eslint.js deleted file mode 100755 index 9c433d0..0000000 --- a/email_app/webpack.config.eslint.js +++ /dev/null @@ -1,3 +0,0 @@ -require('babel-register'); - -module.exports = require('./webpack.config.development'); diff --git a/email_app/webpack.config.node.js b/email_app/webpack.config.node.js deleted file mode 100755 index d4d3651..0000000 --- a/email_app/webpack.config.node.js +++ /dev/null @@ -1,12 +0,0 @@ -// for babel-plugin-webpack-loaders -require('babel-register'); -const devConfigs = require('./webpack.config.development'); - -module.exports = { - output: { - libraryTarget: 'commonjs2' - }, - module: { - loaders: devConfigs.module.loaders.slice(1) // remove babel-loader - } -}; diff --git a/email_app/webpack.config.production.js b/email_app/webpack.config.production.js deleted file mode 100755 index 150f368..0000000 --- a/email_app/webpack.config.production.js +++ /dev/null @@ -1,41 +0,0 @@ -import webpack from 'webpack'; -import ExtractTextPlugin from 'extract-text-webpack-plugin'; -import merge from 'webpack-merge'; -import baseConfig from './webpack.config.base'; - -const config = merge(baseConfig, { - devtool: 'cheap-module-source-map', - - entry: './app/index', - - output: { - publicPath: '../dist/' - }, - - module: { - loaders: [ - { - test: /\.less$/, - loader: "style!css!less" - }, - { test: /\.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, loader: 'file-loader?name=fonts/[name].[ext]' } - ] - }, - plugins: [ - new webpack.optimize.OccurrenceOrderPlugin(), - new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify('production') - }), - new webpack.optimize.UglifyJsPlugin({ - compressor: { - screw_ie8: true, - warnings: false - } - }), - new ExtractTextPlugin('style.css', { allChunks: true }) - ], - - target: 'electron-renderer' -}); - -export default config; From a7d0dd520cde5bd512014052c06eefbdafbd34b6 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Mon, 13 Mar 2017 12:53:17 +0100 Subject: [PATCH 04/44] feat/Dependencis Add new nodejs library --- email_app/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/email_app/package.json b/email_app/package.json index 5de3f86..bb2d4c1 100644 --- a/email_app/package.json +++ b/email_app/package.json @@ -116,6 +116,7 @@ "redux": "^3.6.0", "redux-axios-middleware": "^2.0.0", "redux-thunk": "^2.1.0", + "safe-app": "git+https://github.com/maidsafe/safe_app_nodejs/", "source-map-support": "^0.4.2", "urlsafe-base64": "^1.0.0" }, From 75469bccaffac3537b381d095559e98fce2acd6f Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Mon, 13 Mar 2017 13:31:33 +0100 Subject: [PATCH 05/44] fix/Actions Simplify actions --- .../app/actions/appendable_data_actions.js | 161 ++++-------------- email_app/app/actions/cipher-opts_actions.js | 24 +-- .../app/actions/data_id_handle_actions.js | 72 ++------ .../app/actions/immutable_data_actions.js | 91 ++-------- email_app/app/actions/initializer_actions.js | 8 +- email_app/app/actions/nfs_actions.js | 29 +--- .../app/actions/structured_data_actions.js | 104 +++-------- 7 files changed, 93 insertions(+), 396 deletions(-) diff --git a/email_app/app/actions/appendable_data_actions.js b/email_app/app/actions/appendable_data_actions.js index f54f9f2..226f0b8 100644 --- a/email_app/app/actions/appendable_data_actions.js +++ b/email_app/app/actions/appendable_data_actions.js @@ -1,185 +1,82 @@ import ACTION_TYPES from './actionTypes'; import { CONSTANTS } from '../constants'; -export const createAppendableData = (token, name) => { +export const createAppendableData = (name) => { return { type: ACTION_TYPES.CREATE_APPENDABLE_DATA, - payload: { - request: { - method: 'post', - url: '/appendable-data', - headers: { - 'Authorization': token - }, - data: { - name, - isPrivate: true, - filterType: CONSTANTS.APPENDABLE_DATA_FILTER_TYPE.BLACK_LIST, - filterKeys: [] - } - } - } + name }; }; -export const fetchAppendableDataMeta = (token, handleId) => { +export const fetchAppendableDataMeta = (handleId) => { return { type: ACTION_TYPES.FETCH_APPENDABLE_DATA_META, - payload: { - request: { - url: `/appendable-data/metadata/${handleId}`, - headers: { - 'Authorization': token - } - } - } + handle }; }; -export const fetchAppendableDataHandle = (token, dataIdHandle) => { // id => appendable data id +export const fetchAppendableDataHandle = (dataIdHandle) => { // id => appendable data id return { type: ACTION_TYPES.FETCH_APPENDABLE_DATA_HANDLER, - payload: { - request: { - url: `/appendable-data/handle/${dataIdHandle}`, - headers: { - 'Authorization': token, - 'Is-Private': true - } - } - } + dataIdHandle }; }; -export const fetchDataIdAt = (token, handleId, index) => ({ +export const fetchDataIdAt = (handleId, index) => ({ type: ACTION_TYPES.FETCH_DATA_ID_AT, - payload: { - request: { - url: `/appendable-data/${handleId}/${index}`, - headers: { - 'Authorization': token - } - } - } + handleId, + index }); -export const getEncryptedKey = (token, handleId) => ({ +export const getEncryptedKey = (handleId) => ({ type: ACTION_TYPES.GET_ENCRYPTED_KEY, - payload: { - request: { - url: `/appendable-data/encrypt-key/${handleId}`, - headers: { - 'Authorization': token - } - } - } + handleId }); -export const deleteEncryptedKey = (token, handleId) => ({ +export const deleteEncryptedKey = (handleId) => ({ type: ACTION_TYPES.DELETE_ENCRYPTED_KEY, - payload: { - request: { - method: 'delete', - url: `/appendable-data/encrypt-key/${handleId}`, - headers: { - 'Authorization': token - } - } - } + handleId }); -export const removeFromAppendableData = (token, handleId, index) => { +export const removeFromAppendableData = (handleId, index) => { return { type: ACTION_TYPES.REMOVE_FROM_APPENDABLE_DATA, - payload: { - request: { - method: 'delete', - url: `/appendable-data/${handleId}/${index}`, - headers: { - 'Authorization': token - } - } - } + handleId, + index }; }; -export const appendAppendableData = (token, handleId, dataIdHandle) => ({ +export const appendAppendableData = (handleId, dataIdHandle) => ({ type: ACTION_TYPES.APPEND_APPENDABLE_DATA, - payload: { - request: { - method: 'put', - url: `/appendable-data/${handleId}/${dataIdHandle}`, - headers: { - 'Authorization': token - } - } + handleId, + dataIdHa } }); -export const clearDeletedData = (token, handleId) => ({ +export const clearDeletedData = (handleId) => ({ type: ACTION_TYPES.CLEAR_DELETE_DATA, - payload: { - request: { - method: 'delete', - url: `/appendable-data/clear-deleted-data/${handleId}`, - headers: { - 'Authorization': token - } - } - } + handleId }); -export const getAppendableDataLength = (token, handleId) => ({ +export const getAppendableDataLength = (handleId) => ({ type: ACTION_TYPES.GET_APPENDABLE_DATA_LENGTH, - payload: { - request: { - url: `/appendable-data/serialise/${handleId}`, - headers: { - 'Authorization': token - }, - responseType: 'arraybuffer' - } - } + handleId }); -export const dropAppendableDataHandle = (token, handleId) => ({ +export const dropAppendableDataHandle = (handleId) => ({ type: ACTION_TYPES.DROP_APPENDABLE_DATA_HANDLE, - payload: { - request: { - method: 'delete', - url: `/appendable-data/handle/${handleId}`, - headers: { - 'Authorization': token - } - } - } + handleId }); -export const postAppendableData = (token, handleId) => ({ +export const postAppendableData = (handleId) => ({ type: ACTION_TYPES.POST_APPENDABLE_DATA, - payload: { - request: { - method: 'post', - url: `/appendable-data/${handleId}`, - headers: { - 'Authorization': token - } - } - } + handleId }); -export const putAppendableData = (token, handleId) => ({ +export const putAppendableData = (handleId) => ({ type: ACTION_TYPES.PUT_APPENDABLE_DATA, - payload: { - request: { - method: 'put', - url: `/appendable-data/${handleId}`, - headers: { - 'Authorization': token - } - } - } + handleId }); export const setAppendableDataId = (id) => ({ diff --git a/email_app/app/actions/cipher-opts_actions.js b/email_app/app/actions/cipher-opts_actions.js index 2ae8c66..a1b492f 100644 --- a/email_app/app/actions/cipher-opts_actions.js +++ b/email_app/app/actions/cipher-opts_actions.js @@ -1,26 +1,12 @@ import ACTION_TYPES from './actionTypes'; -export const getCipherOptsHandle = (token, encType, keyHandle='') => ({ +export const getCipherOptsHandle = (encType, keyHandle='') => ({ type: ACTION_TYPES.GET_CIPHER_OPTS_HANDLE, - payload: { - request: { - url: `/cipher-opts/${encType}/${keyHandle}`, - headers: { - 'Authorization': token, - } - } - } + encType, + keyHandle }); -export const deleteCipherOptsHandle = (token, handleId) => ({ +export const deleteCipherOptsHandle = (handleId) => ({ type: ACTION_TYPES.DELETE_CIPHER_OPTS_HANDLE, - payload: { - request: { - method: 'delete', - url: `/cipher-opts/${handleId}`, - headers: { - 'Authorization': token, - } - } - } + handleId }); diff --git a/email_app/app/actions/data_id_handle_actions.js b/email_app/app/actions/data_id_handle_actions.js index 09b2cd2..00f751d 100644 --- a/email_app/app/actions/data_id_handle_actions.js +++ b/email_app/app/actions/data_id_handle_actions.js @@ -1,77 +1,29 @@ import ACTION_TYPES from './actionTypes'; import * as base64 from 'urlsafe-base64'; -export const getStructuredDataIdHandle = (token, name, typeTag) => ({ +export const getStructuredDataIdHandle = (name, typeTag) => ({ type: ACTION_TYPES.GET_STRUCTURED_DATA_ID_HANDLE, - payload: { - request: { - method: 'post', - url: '/data-id/structured-data', - headers: { - 'Authorization': token - }, - data: { - typeTag, - name - } - } - } + typeTag, + name }); -export const getAppendableDataIdHandle = (token, name) => ({ +export const getAppendableDataIdHandle = (name) => ({ type: ACTION_TYPES.GET_STRUCTURED_DATA_ID_HANDLE, - payload: { - request: { - method: 'post', - url: '/data-id/appendable-data', - headers: { - 'Authorization': token - }, - data: { - isPrivate: true, - name - } - } - } + isPrivate: true, + name }); -export const serialiseDataId = (token, handleId) => ({ +export const serialiseDataId = (handleId) => ({ type: ACTION_TYPES.SERIALISE_DATA_ID, - payload: { - request: { - url: `/data-id/${handleId}`, - headers: { - 'Authorization': token - }, - responseType: 'arraybuffer' - } - } + handleId }); -export const deserialiseDataId = (token, data) => ({ +export const deserialiseDataId = (data) => ({ type: ACTION_TYPES.DESERIALISE_DATA_ID, - payload: { - request: { - method: 'post', - url: '/data-id', - headers: { - 'Content-Type': 'text/plain', - 'Authorization': token - }, - data: new Uint8Array(base64.decode(data)) - } - } + data }); -export const dropHandler = (token, handleId) => ({ +export const dropHandler = (handleId) => ({ type: ACTION_TYPES.DROP_HANDLER, - payload: { - request: { - method: 'delete', - url: `/data-id/${handleId}`, - headers: { - 'Authorization': token - } - } - } + handleId }); diff --git a/email_app/app/actions/immutable_data_actions.js b/email_app/app/actions/immutable_data_actions.js index 2a07c86..3b0dc3f 100644 --- a/email_app/app/actions/immutable_data_actions.js +++ b/email_app/app/actions/immutable_data_actions.js @@ -1,95 +1,40 @@ import ACTION_TYPES from './actionTypes'; -export const createImmutableDataWriterHandle = (token) => ({ - type: ACTION_TYPES.CREATE_IMMUT_WRITER_HANDLE, - payload: { - request: { - url: `/immutable-data/writer`, - headers: { - 'Authorization': token - } - } - } +export const createImmutableDataWriterHandle = () => ({ + type: ACTION_TYPES.CREATE_IMMUT_WRITER_HANDLE }); -export const getImmutableDataReadHandle = (token, handleId) => ({ - type: ACTION_TYPES.GET_IMMUT_READ_HANDLE, - payload: { - request: { - url: `/immutable-data/reader/${handleId}`, - headers: { - 'Authorization': token - } - } - } +export const getImmutableDataReadHandle = (handleId) => ({ + type: ACTION_TYPES.GET_IMMUT_READ_HANDLE + handleId }); -export const readImmutableData = (token, handleId) => ({ +export const readImmutableData = (handleId) => ({ type: ACTION_TYPES.READ_IMMUT_DATA, - payload: { - request: { - url: `/immutable-data/${handleId}`, - headers: { - 'Authorization': token - }, - responseType: 'arraybuffer' - } - } + handleId }); -export const writeImmutableData = (token, handleId, data) => { - const dataByteArray = new Uint8Array(new Buffer(JSON.stringify(data))); +export const writeImmutableData = (handleId, data) => { + // const dataByteArray = new Uint8Array(new Buffer(JSON.stringify(data))); return { type: ACTION_TYPES.WRITE_IMMUT_DATA, - payload: { - request: { - method: 'post', - url: `/immutable-data/${handleId}`, - headers: { - 'content-type': 'text/plain', - 'Authorization': token - }, - data: dataByteArray - } - } + handleId, + data }; }; -export const putImmutableData = (token, handleId, cipherOptsHandle) => ({ +export const putImmutableData = (handleId, cipherOptsHandle) => ({ type: ACTION_TYPES.PUT_IMMUT_DATA, - payload: { - request: { - method: 'put', - url: `/immutable-data/${handleId}/${cipherOptsHandle}`, - headers: { - 'Authorization': token, - } - } - } + handleId, + cipherOptsHandle }); -export const closeImmutableDataWriter = (token, handleId) => ({ +export const closeImmutableDataWriter = (handleId) => ({ type: ACTION_TYPES.CLOSE_IMMUT_DATA_WRITER, - payload: { - request: { - method: 'delete', - url: `/immutable-data/writer/${handleId}`, - headers: { - 'Authorization': token, - } - } - } + handleId }); -export const closeImmutableDataReader = (token, handleId) => ({ +export const closeImmutableDataReader = (handleId) => ({ type: ACTION_TYPES.CLOSE_IMMUT_DATA_READER, - payload: { - request: { - method: 'delete', - url: `/immutable-data/reader/${handleId}`, - headers: { - 'Authorization': token, - } - } - } + handleId }); diff --git a/email_app/app/actions/initializer_actions.js b/email_app/app/actions/initializer_actions.js index 82a9728..a025e3d 100644 --- a/email_app/app/actions/initializer_actions.js +++ b/email_app/app/actions/initializer_actions.js @@ -8,13 +8,7 @@ export const setInitializerTask = (task) => ({ export const authoriseApplication = (data) => { return { type: ACTION_TYPES.AUTHORISE_APP, - payload: { - request: { - method: 'post', - url: '/auth', - data: data - } - } + data }; }; diff --git a/email_app/app/actions/nfs_actions.js b/email_app/app/actions/nfs_actions.js index f3aa4c8..71e7500 100644 --- a/email_app/app/actions/nfs_actions.js +++ b/email_app/app/actions/nfs_actions.js @@ -1,35 +1,16 @@ import ACTION_TYPES from './actionTypes'; import { CONSTANTS } from '../constants'; -export const writeConfigFile = (token, coreId) => { +export const writeConfigFile = (coreId) => { + // FIXME: are these even needed? return { type: ACTION_TYPES.WRITE_CONFIG_FILE, - payload: { - request: { - method: 'post', - url: `/nfs/file/${CONSTANTS.ROOT_PATH}/config`, - headers: { - 'Authorization': token, - 'Content-Type': 'plain/text' - }, - data: new Uint8Array(Buffer(coreId)) - } - } + coreId } }; -export const getConfigFile = token => { +export const getConfigFile = () => { return { - type: ACTION_TYPES.GET_CONFIG_FILE, - payload: { - request: { - url: `/nfs/file/${CONSTANTS.ROOT_PATH}/config`, - headers: { - 'Authorization': token, - Range: 'bytes=0-' - }, - responseType: 'arraybuffer' - } - } + type: ACTION_TYPES.GET_CONFIG_FILE }; }; diff --git a/email_app/app/actions/structured_data_actions.js b/email_app/app/actions/structured_data_actions.js index 7883c71..1b79e84 100644 --- a/email_app/app/actions/structured_data_actions.js +++ b/email_app/app/actions/structured_data_actions.js @@ -1,115 +1,57 @@ import ACTION_TYPES from './actionTypes'; import { CONSTANTS } from '../constants'; -export const createStructuredData = (token, name, data, cipherHandle) => ({ +export const createStructuredData = (name, data, cipherHandle) => ({ type: ACTION_TYPES.CREATE_STRUCTURED_DATA, - payload: { - request: { - method: 'post', - url: '/structured-data', - headers: { - 'Authorization': token - }, - data: { - name, - typeTag: CONSTANTS.TAG_TYPE.DEFAULT, - cipherOpts: cipherHandle, - data: new Buffer(JSON.stringify(data)).toString('base64') - } - } + name, + typeTag: CONSTANTS.TAG_TYPE.DEFAULT, + cipherOpts: cipherHandle, + data + // data: new Buffer(JSON.stringify(data)).toString('base64') } }); -export const fetchStructuredData = (token, handleId) => ({ +export const fetchStructuredData = (handleId) => ({ type: ACTION_TYPES.FETCH_STRUCTURED_DATA, - payload: { - request: { - url: `/structured-data/${handleId}`, - headers: { - 'Authorization': token, - 'Content-Type': 'text/plain' - } - } - } + handleId }); -export const fetchStructuredDataHandle = (token, dataIdHandle) => ({ +export const fetchStructuredDataHandle = (dataIdHandle) => ({ type: ACTION_TYPES.FETCH_STRUCTURE_DATA_HANDLE, - payload: { - request: { - url: `/structured-data/handle/${dataIdHandle}`, - headers: { - 'Authorization': token - } - } + dataIdHandle } }); -export const fetchStructuredDataIdHandle = (token, handleId) => ({ +export const fetchStructuredDataIdHandle = (handleId) => ({ type: ACTION_TYPES.FETCH_STRUCTURE_DATA_ID_HANDLE, - payload: { - request: { - url: `/structured-data/data-id/${handleId}`, - headers: { - 'Authorization': token - } - } + handleId } }); -export const updateStructuredData = (token, handleId, data, cipherOpts) => ({ +export const updateStructuredData = (handleId, data, cipherOpts) => ({ type: ACTION_TYPES.UPDATE_STRUCTURED_DATA, - payload: { - request: { - method: 'patch', - url: `/structured-data/${handleId}`, - headers: { - 'Authorization': token - }, - data: { - cipherOpts, - data: new Buffer(JSON.stringify(data)).toString('base64') - } - } + handleId + cipherOpts, + data + // data: new Buffer(JSON.stringify(data)).toString('base64') } }); -export const dropStructuredDataHandle = (token, handleId) => ({ +export const dropStructuredDataHandle = (handleId) => ({ type: ACTION_TYPES.DROP_STRUCTURED_DATA_HANDLE, - payload: { - request: { - method: 'delete', - url: `/structured-data/handle/${handleId}`, - headers: { - 'Authorization': token - } - } + handleId } }); -export const postStructuredData = (token, handleId) => ({ +export const postStructuredData = (handleId) => ({ type: ACTION_TYPES.POST_STRUCTURED_DATA, - payload: { - request: { - method: 'post', - url: `/structured-data/${handleId}`, - headers: { - 'Authorization': token - } - } + handleId } }); -export const putStructuredData = (token, handleId) => ({ +export const putStructuredData = (handleId) => ({ type: ACTION_TYPES.PUT_STRUCTURED_DATA, - payload: { - request: { - method: 'put', - url: `/structured-data/${handleId}`, - headers: { - 'Authorization': token - } - } + handleId } }); From 3449090de9b181ecc1c48e7b980cae6241174be9 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Wed, 15 Mar 2017 16:16:35 +0100 Subject: [PATCH 06/44] feat/Auth Implement fake auth-flow --- .../app/actions/appendable_data_actions.js | 15 ++++--- .../app/actions/immutable_data_actions.js | 2 +- email_app/app/actions/initializer_actions.js | 33 ++++++++++++++- .../app/actions/structured_data_actions.js | 9 +--- email_app/app/app.js | 15 +++++++ email_app/app/components/initializer.js | 42 ++++++++++--------- email_app/app/constants.js | 12 ++---- .../app/containers/initializer_container.js | 22 +++++----- email_app/app/index.js | 32 +++++++++++++- email_app/app/reducers/initialiser.js | 4 +- .../app/store/configureStore.development.js | 11 ++--- .../app/store/configureStore.production.js | 12 ++---- email_app/package.json | 3 ++ 13 files changed, 135 insertions(+), 77 deletions(-) diff --git a/email_app/app/actions/appendable_data_actions.js b/email_app/app/actions/appendable_data_actions.js index 226f0b8..f76569c 100644 --- a/email_app/app/actions/appendable_data_actions.js +++ b/email_app/app/actions/appendable_data_actions.js @@ -48,35 +48,34 @@ export const removeFromAppendableData = (handleId, index) => { export const appendAppendableData = (handleId, dataIdHandle) => ({ type: ACTION_TYPES.APPEND_APPENDABLE_DATA, - handleId, - dataIdHa - } + handleId, + dataIdHa }); export const clearDeletedData = (handleId) => ({ type: ACTION_TYPES.CLEAR_DELETE_DATA, - handleId + handleId }); export const getAppendableDataLength = (handleId) => ({ type: ACTION_TYPES.GET_APPENDABLE_DATA_LENGTH, - handleId + handleId }); export const dropAppendableDataHandle = (handleId) => ({ type: ACTION_TYPES.DROP_APPENDABLE_DATA_HANDLE, - handleId + handleId }); export const postAppendableData = (handleId) => ({ type: ACTION_TYPES.POST_APPENDABLE_DATA, - handleId + handleId }); export const putAppendableData = (handleId) => ({ type: ACTION_TYPES.PUT_APPENDABLE_DATA, - handleId + handleId }); export const setAppendableDataId = (id) => ({ diff --git a/email_app/app/actions/immutable_data_actions.js b/email_app/app/actions/immutable_data_actions.js index 3b0dc3f..1e75ad3 100644 --- a/email_app/app/actions/immutable_data_actions.js +++ b/email_app/app/actions/immutable_data_actions.js @@ -5,7 +5,7 @@ export const createImmutableDataWriterHandle = () => ({ }); export const getImmutableDataReadHandle = (handleId) => ({ - type: ACTION_TYPES.GET_IMMUT_READ_HANDLE + type: ACTION_TYPES.GET_IMMUT_READ_HANDLE, handleId }); diff --git a/email_app/app/actions/initializer_actions.js b/email_app/app/actions/initializer_actions.js index a025e3d..49dc94f 100644 --- a/email_app/app/actions/initializer_actions.js +++ b/email_app/app/actions/initializer_actions.js @@ -1,14 +1,43 @@ + +import { initializeApp, fromAuthURI } from 'safe-app'; + import ACTION_TYPES from './actionTypes'; + +var authResolver; +var authRejecter; +const authPromise = new Promise((resolve, reject) => { + authResolver = resolve; + authRejecter = reject; +}); + export const setInitializerTask = (task) => ({ type: ACTION_TYPES.SET_INITIALIZER_TASK, task }); -export const authoriseApplication = (data) => { +export const receiveResponse = (uri) => { + return { + type: ACTION_TYPES.AUTHORISE_APP, + payload: fromAuthURI(uri) + .then((app) => authResolver ? authResolver(app) : app) + } + +}; + +export const authoriseApplication = (appData, permissions) => { + initializeApp(appData) + .then((app) => + process.env.SAFE_FAKE_AUTH + ? app.auth.loginForTest(permissions) + .then(authResolver) + : app.auth.genAuthUri({}) + .then(resp => app.auth.openUri(resp.uri)) + ).catch(authRejecter) + return { type: ACTION_TYPES.AUTHORISE_APP, - data + payload: authPromise }; }; diff --git a/email_app/app/actions/structured_data_actions.js b/email_app/app/actions/structured_data_actions.js index 1b79e84..1ceba81 100644 --- a/email_app/app/actions/structured_data_actions.js +++ b/email_app/app/actions/structured_data_actions.js @@ -8,7 +8,6 @@ export const createStructuredData = (name, data, cipherHandle) => ({ cipherOpts: cipherHandle, data // data: new Buffer(JSON.stringify(data)).toString('base64') - } }); export const fetchStructuredData = (handleId) => ({ @@ -19,39 +18,33 @@ export const fetchStructuredData = (handleId) => ({ export const fetchStructuredDataHandle = (dataIdHandle) => ({ type: ACTION_TYPES.FETCH_STRUCTURE_DATA_HANDLE, dataIdHandle - } }); export const fetchStructuredDataIdHandle = (handleId) => ({ type: ACTION_TYPES.FETCH_STRUCTURE_DATA_ID_HANDLE, handleId - } }); export const updateStructuredData = (handleId, data, cipherOpts) => ({ type: ACTION_TYPES.UPDATE_STRUCTURED_DATA, - handleId + handleId, cipherOpts, data // data: new Buffer(JSON.stringify(data)).toString('base64') - } }); export const dropStructuredDataHandle = (handleId) => ({ type: ACTION_TYPES.DROP_STRUCTURED_DATA_HANDLE, handleId - } }); export const postStructuredData = (handleId) => ({ type: ACTION_TYPES.POST_STRUCTURED_DATA, handleId - } }); export const putStructuredData = (handleId) => ({ type: ACTION_TYPES.PUT_STRUCTURED_DATA, handleId - } }); diff --git a/email_app/app/app.js b/email_app/app/app.js index 9305b50..2659747 100644 --- a/email_app/app/app.js +++ b/email_app/app/app.js @@ -3,12 +3,27 @@ import { render } from 'react-dom'; import { Provider } from 'react-redux'; import { Router, hashHistory } from 'react-router'; import { syncHistoryWithStore } from 'react-router-redux'; +import { ipcRenderer as ipc } from 'electron'; import routes from './routes'; import configureStore from './store/configureStore'; +import { receiveResponse } from "./actions/initializer_actions"; const store = configureStore(); const history = syncHistoryWithStore(hashHistory, store); + +const listenForAuthReponse = (event, response) => { + // TODO parse response + if (response) { + store.dispatch(receiveResponse(response)); // TODO do it concurrently (no to linked dispatch) + } else { + // store.dispatch(onAuthFailure(new Error('Authorisation failed'))); + } +}; + +ipc.on('auth-response', listenForAuthReponse); + + export default class App extends React.Component { render() { return ( diff --git a/email_app/app/components/initializer.js b/email_app/app/components/initializer.js index 14d0d9f..5a9d298 100644 --- a/email_app/app/components/initializer.js +++ b/email_app/app/components/initializer.js @@ -30,23 +30,25 @@ export default class Initializer extends Component { componentDidMount() { const { authoriseApplication, setInitializerTask } = this.props; - authoriseApplication(AUTH_PAYLOAD) - .then(() => { + authoriseApplication(AUTH_PAYLOAD, {"_publicNames" : ["Insert"]}) + .then((app) => { + console.log(app); setInitializerTask(MESSAGES.INITIALIZE.CHECK_CONFIGURATION); return this.getConfiguration(); }) - .catch(() => { + .catch((err) => { + console.error(err) return showDialog('Authorisation Error', MESSAGES.AUTHORISATION_ERROR); }); } getConfiguration() { - const { token, getConfigFile, setInitializerTask } = this.props; - if (!token) { - throw new Error('Application token not found.'); + const { client, getConfigFile, setInitializerTask } = this.props; + if (!client) { + throw new Error('Application client not found.'); } - getConfigFile(token) + getConfigFile(client) .then(res => { if (res.error) { setInitializerTask(MESSAGES.INITIALIZE.CREATE_CORE_STRUCTURE); @@ -58,9 +60,9 @@ export default class Initializer extends Component { } getStructuredDataIdHandle(name) { - const { token, getStructuredDataIdHandle } = this.props; + const { client, getStructuredDataIdHandle } = this.props; - getStructuredDataIdHandle(token, name, CONSTANTS.TAG_TYPE.DEFAULT) + getStructuredDataIdHandle(client, name, CONSTANTS.TAG_TYPE.DEFAULT) .then((res) => { if (res.error) { return showDialog('Get Structure Data Handler Error', res.error.message); @@ -70,8 +72,8 @@ export default class Initializer extends Component { } getStructuredDataHandle(handleId) { - const { token, fetchStructuredDataHandle } = this.props; - fetchStructuredDataHandle(token, handleId) + const { client, fetchStructuredDataHandle } = this.props; + fetchStructuredDataHandle(client, handleId) .then(res => { if (res.error) { return showDialog('Get Structure Data Handler Error', res.error.message); @@ -81,8 +83,8 @@ export default class Initializer extends Component { } fetchStructuredData(handleId) { - const { token, fetchStructuredData } = this.props; - fetchStructuredData(token, handleId) + const { client, fetchStructuredData } = this.props; + fetchStructuredData(client, handleId) .then(res => { if (res.error) { return showDialog('Get Structure Data Error', res.error.message); @@ -96,7 +98,7 @@ export default class Initializer extends Component { } createStructuredData() { - const { token, createStructuredData, putStructuredData, getCipherOptsHandle, deleteCipherOptsHandle, dropStructuredDataHandle, setInitializerTask } = this.props; + const { client, createStructuredData, putStructuredData, getCipherOptsHandle, deleteCipherOptsHandle, dropStructuredDataHandle, setInitializerTask } = this.props; this.createCoreCount++; const structuredDataId = generateStructredDataId(); const data = { @@ -106,7 +108,7 @@ export default class Initializer extends Component { }; const deleteCipherHandle = (handleId) => { - deleteCipherOptsHandle(token, handleId) + deleteCipherOptsHandle(client, handleId) .then((res) => { if (res.error) { return showDialog('Delete Cipher Opts Handle Error', res.error.message); @@ -116,7 +118,7 @@ export default class Initializer extends Component { }; const put = (handleId) => { - putStructuredData(token, handleId) + putStructuredData(client, handleId) .then((res) => { if (res.error) { return showDialog('Put Structure Data Error', res.error.message); @@ -126,7 +128,7 @@ export default class Initializer extends Component { }; const create = (cipherOptsHandle) => { - createStructuredData(token, structuredDataId, data, cipherOptsHandle) + createStructuredData(client, structuredDataId, data, cipherOptsHandle) .then((res) => { if (res.error) { if (this.createCoreCount > 5) { @@ -141,7 +143,7 @@ export default class Initializer extends Component { }; const getCipherHandle = () => { - getCipherOptsHandle(token, CONSTANTS.ENCRYPTION.SYMMETRIC) + getCipherOptsHandle(client, CONSTANTS.ENCRYPTION.SYMMETRIC) .then((res) => { if (res.error) { return showDialog('Get Cipher Opts Handle Error', res.error.message); @@ -154,8 +156,8 @@ export default class Initializer extends Component { } writeConfigFile(structuredDataId) { - const { token, writeConfigFile } = this.props; - writeConfigFile(token, structuredDataId) + const { client, writeConfigFile } = this.props; + writeConfigFile(client, structuredDataId) .then((res) => { if (res.error) { return showDialog('Write Configuration File Error', res.error.message); diff --git a/email_app/app/constants.js b/email_app/app/constants.js index 726fb75..8bd93bc 100644 --- a/email_app/app/constants.js +++ b/email_app/app/constants.js @@ -2,8 +2,6 @@ import pkg from '../package.json'; export const CONSTANTS = { LOCAL_AUTH_DATA_KEY: 'local_auth_data_key', - SERVER_URL: 'http://localhost:8100', - ROOT_PATH: 'APP', TAG_TYPE: { DEFAULT: 500, VERSIONED: 501 @@ -42,11 +40,7 @@ export const MESSAGES = { }; export const AUTH_PAYLOAD = { - app: { - name: pkg.productName, - vendor: pkg.author.name, - version: pkg.version, - id: pkg.identifier - }, - permissions: ['LOW_LEVEL_API'] + id: pkg.identifier, + name: pkg.productName, + vendor: pkg.vendor }; diff --git a/email_app/app/containers/initializer_container.js b/email_app/app/containers/initializer_container.js index 833e636..b953e0c 100644 --- a/email_app/app/containers/initializer_container.js +++ b/email_app/app/containers/initializer_container.js @@ -8,7 +8,7 @@ import { getCipherOptsHandle, deleteCipherOptsHandle } from '../actions/cipher-o const mapStateToProps = state => { return { - token: state.initializer.token, + client: state.initializer.client, tasks: state.initializer.tasks, config: state.initializer.config, coreData: state.initializer.coreData @@ -19,16 +19,16 @@ const mapDispatchToProps = dispatch => { return { setInitializerTask: task => (dispatch(setInitializerTask(task))), authoriseApplication: payload => (dispatch(authoriseApplication(payload))), - getConfigFile: (token) => (dispatch(getConfigFile(token))), - writeConfigFile: (token, data) => (dispatch(writeConfigFile(token, data))), - getCipherOptsHandle: (token, encType, keyHandle) => (dispatch(getCipherOptsHandle(token, encType, keyHandle))), - deleteCipherOptsHandle: (token, handleId) => (dispatch(deleteCipherOptsHandle(token, handleId))), - getStructuredDataIdHandle: (token, name, typeTag) => (dispatch(getStructuredDataIdHandle(token, name, typeTag))), - createStructuredData: (token, name, data) => (dispatch(createStructuredData(token, name, data))), - fetchStructuredData: (token, handleId) => (dispatch(fetchStructuredData(token, handleId))), - fetchStructuredDataHandle: (token, dataIdHandle) => (dispatch(fetchStructuredDataHandle(token, dataIdHandle))), - putStructuredData: (token, handleId) => (dispatch(putStructuredData(token, handleId))), - dropStructuredDataHandle: (token, handleId) => (dispatch(dropStructuredDataHandle(token, handleId))) + getConfigFile: (client) => (dispatch(getConfigFile(client))), + writeConfigFile: (client, data) => (dispatch(writeConfigFile(client, data))), + getCipherOptsHandle: (client, encType, keyHandle) => (dispatch(getCipherOptsHandle(client, encType, keyHandle))), + deleteCipherOptsHandle: (client, handleId) => (dispatch(deleteCipherOptsHandle(client, handleId))), + getStructuredDataIdHandle: (client, name, typeTag) => (dispatch(getStructuredDataIdHandle(client, name, typeTag))), + createStructuredData: (client, name, data) => (dispatch(createStructuredData(client, name, data))), + fetchStructuredData: (client, handleId) => (dispatch(fetchStructuredData(client, handleId))), + fetchStructuredDataHandle: (client, dataIdHandle) => (dispatch(fetchStructuredDataHandle(client, dataIdHandle))), + putStructuredData: (client, handleId) => (dispatch(putStructuredData(client, handleId))), + dropStructuredDataHandle: (client, handleId) => (dispatch(dropStructuredDataHandle(client, handleId))) }; }; diff --git a/email_app/app/index.js b/email_app/app/index.js index 0803d5b..75cef93 100755 --- a/email_app/app/index.js +++ b/email_app/app/index.js @@ -8,6 +8,11 @@ require("babel-polyfill"); // be closed automatically when the JavaScript object is garbage collected. let mainWindow; + +const sendResponse = (success) => { + mainWindow.webContents.send('auth-response', success ? success : ''); +}; + const isDevMode = process.execPath.match(/[\\/]electron/); // if (isDevMode) enableLiveReload({strategy: 'react-hmr'}); @@ -40,7 +45,28 @@ const createWindow = async () => { // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. -app.on('ready', createWindow); + + +app.on('ready', async () => { + await createWindow(); + + const shouldQuit = app.makeSingleInstance(function(commandLine) { + if (commandLine.length >= 2 && commandLine[1]) { + sendResponse(commandLine[1]); + } + + // Someone tried to run a second instance, we should focus our window + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + } + }); + + if (shouldQuit) { + app.quit(); + } +}); + // Quit when all windows are closed. app.on('window-all-closed', () => { @@ -61,3 +87,7 @@ app.on('activate', () => { // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and import them here. + +app.on('open-url', function (e, url) { + sendResponse(url); +}); \ No newline at end of file diff --git a/email_app/app/reducers/initialiser.js b/email_app/app/reducers/initialiser.js index edd2ab1..7901b1f 100644 --- a/email_app/app/reducers/initialiser.js +++ b/email_app/app/reducers/initialiser.js @@ -2,7 +2,7 @@ import ACTION_TYPES from '../actions/actionTypes'; import { MESSAGES } from '../constants'; const initialState = { - token: '', + client: '', tasks: [], config: null, coreData: { @@ -24,7 +24,7 @@ const initializer = (state = initialState, action) => { break; } case `${ACTION_TYPES.AUTHORISE_APP}_SUCCESS`: - return { ...state, token: `Bearer ${action.payload.data.token}` }; + return { ...state, client: action.payload }; break; case ACTION_TYPES.SET_INITIALIZER_TASK: const tasks = state.tasks.slice(); diff --git a/email_app/app/store/configureStore.development.js b/email_app/app/store/configureStore.development.js index 72d6001..aca2d7d 100755 --- a/email_app/app/store/configureStore.development.js +++ b/email_app/app/store/configureStore.development.js @@ -1,14 +1,12 @@ import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; import createLogger from 'redux-logger'; -import axios from 'axios'; -import axiosMiddleware from 'redux-axios-middleware'; import { hashHistory } from 'react-router'; import { routerMiddleware, push } from 'react-router-redux'; import rootReducer from '../reducers'; import { CONSTANTS } from '../constants'; +import promiseMiddleware from 'redux-promise-middleware'; -// import * as counterActions from '../actions/counter'; const actionCreators = { // ...counterActions, @@ -22,12 +20,11 @@ const logger = createLogger({ const router = routerMiddleware(hashHistory); -const client = axios.create({ - baseURL: CONSTANTS.SERVER_URL -}); const enhancer = compose( - applyMiddleware(thunk, router, logger, axiosMiddleware(client)), + applyMiddleware(thunk, router, logger, promiseMiddleware({ + promiseTypeSuffixes: ['LOADING', 'SUCCESS', 'ERROR'] + })), window.devToolsExtension ? window.devToolsExtension({ actionCreators }) : noop => noop diff --git a/email_app/app/store/configureStore.production.js b/email_app/app/store/configureStore.production.js index 7d02ed3..206acad 100755 --- a/email_app/app/store/configureStore.production.js +++ b/email_app/app/store/configureStore.production.js @@ -1,20 +1,16 @@ import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import { hashHistory } from 'react-router'; -import axios from 'axios'; -import axiosMiddleware from 'redux-axios-middleware'; import { routerMiddleware } from 'react-router-redux'; import rootReducer from '../reducers'; import { CONSTANTS } from '../constants'; +import promiseMiddleware from 'redux-promise-middleware'; const router = routerMiddleware(hashHistory); -const client = axios.create({ - baseURL: CONSTANTS.SERVER_URL, - responseType: 'json' -}); - -const enhancer = applyMiddleware(thunk, router, axiosMiddleware(client)); +const enhancer = applyMiddleware(thunk, router, promiseMiddleware({ + promiseTypeSuffixes: ['LOADING', 'SUCCESS', 'ERROR'] + })); export default function configureStore(initialState) { return createStore(rootReducer, initialState, enhancer); diff --git a/email_app/package.json b/email_app/package.json index bb2d4c1..9105f9c 100644 --- a/email_app/package.json +++ b/email_app/package.json @@ -4,6 +4,7 @@ "version": "0.1.2", "description": "Mailing application tutorial using SAFE Network", "identifier": "net.maidsafe.mailtutorial", + "vendor": "MaidSafe Ltd.", "main": "app/index.js", "scripts": { "test": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 mocha --compilers js:babel-register --recursive --require ./test/setup.js test/**/*.spec.js", @@ -67,6 +68,7 @@ "del": "^2.2.2", "devtron": "^1.3.0", "electron-devtools-installer": "^2.0.1", + "electron-forge": "^2.8.3", "electron-prebuilt-compile": "1.6.2", "electron-rebuild": "^1.2.0", "eslint": "^3.3.1", @@ -115,6 +117,7 @@ "react-router-redux": "^4.0.8", "redux": "^3.6.0", "redux-axios-middleware": "^2.0.0", + "redux-promise-middleware": "^4.2.0", "redux-thunk": "^2.1.0", "safe-app": "git+https://github.com/maidsafe/safe_app_nodejs/", "source-map-support": "^0.4.2", From 29ac7d04dcb57384dc5f335ad9b5542c005dbac0 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Mon, 27 Mar 2017 12:13:40 +0200 Subject: [PATCH 07/44] fix/Email Cleaning up crufty actions --- .../app/actions/appendable_data_actions.js | 84 -------- email_app/app/actions/cipher-opts_actions.js | 12 -- .../app/actions/create_account_actions.js | 10 - .../app/actions/data_id_handle_actions.js | 29 --- .../app/actions/immutable_data_actions.js | 40 ---- .../app/actions/structured_data_actions.js | 50 ----- email_app/app/components/compose_mail.js | 103 ---------- email_app/app/components/create_account.js | 76 ------- email_app/app/components/initializer.js | 116 +---------- email_app/app/components/mail_inbox.js | 142 +------------ email_app/app/components/mail_list.js | 194 ------------------ .../app/containers/compose_mail_container.js | 19 -- .../containers/create_account_component.js | 11 - .../app/containers/initializer_container.js | 14 -- .../app/containers/mail_inbox_container.js | 34 --- .../app/containers/mail_saved_container.js | 15 -- email_app/app/utils/app_utils.js | 4 - 17 files changed, 2 insertions(+), 951 deletions(-) delete mode 100644 email_app/app/actions/appendable_data_actions.js delete mode 100644 email_app/app/actions/cipher-opts_actions.js delete mode 100644 email_app/app/actions/create_account_actions.js delete mode 100644 email_app/app/actions/data_id_handle_actions.js delete mode 100644 email_app/app/actions/immutable_data_actions.js delete mode 100644 email_app/app/actions/structured_data_actions.js diff --git a/email_app/app/actions/appendable_data_actions.js b/email_app/app/actions/appendable_data_actions.js deleted file mode 100644 index f76569c..0000000 --- a/email_app/app/actions/appendable_data_actions.js +++ /dev/null @@ -1,84 +0,0 @@ -import ACTION_TYPES from './actionTypes'; -import { CONSTANTS } from '../constants'; - -export const createAppendableData = (name) => { - return { - type: ACTION_TYPES.CREATE_APPENDABLE_DATA, - name - }; -}; - -export const fetchAppendableDataMeta = (handleId) => { - return { - type: ACTION_TYPES.FETCH_APPENDABLE_DATA_META, - handle - }; -}; - -export const fetchAppendableDataHandle = (dataIdHandle) => { // id => appendable data id - return { - type: ACTION_TYPES.FETCH_APPENDABLE_DATA_HANDLER, - dataIdHandle - }; -}; - -export const fetchDataIdAt = (handleId, index) => ({ - type: ACTION_TYPES.FETCH_DATA_ID_AT, - handleId, - index -}); - -export const getEncryptedKey = (handleId) => ({ - type: ACTION_TYPES.GET_ENCRYPTED_KEY, - handleId -}); - -export const deleteEncryptedKey = (handleId) => ({ - type: ACTION_TYPES.DELETE_ENCRYPTED_KEY, - handleId -}); - -export const removeFromAppendableData = (handleId, index) => { - return { - type: ACTION_TYPES.REMOVE_FROM_APPENDABLE_DATA, - handleId, - index - }; -}; - -export const appendAppendableData = (handleId, dataIdHandle) => ({ - type: ACTION_TYPES.APPEND_APPENDABLE_DATA, - handleId, - dataIdHa -}); - -export const clearDeletedData = (handleId) => ({ - type: ACTION_TYPES.CLEAR_DELETE_DATA, - handleId -}); - -export const getAppendableDataLength = (handleId) => ({ - type: ACTION_TYPES.GET_APPENDABLE_DATA_LENGTH, - handleId -}); - -export const dropAppendableDataHandle = (handleId) => ({ - type: ACTION_TYPES.DROP_APPENDABLE_DATA_HANDLE, - handleId -}); - - -export const postAppendableData = (handleId) => ({ - type: ACTION_TYPES.POST_APPENDABLE_DATA, - handleId -}); - -export const putAppendableData = (handleId) => ({ - type: ACTION_TYPES.PUT_APPENDABLE_DATA, - handleId -}); - -export const setAppendableDataId = (id) => ({ - type: ACTION_TYPES.SET_APPENDABLE_DATA_ID, - id -}); diff --git a/email_app/app/actions/cipher-opts_actions.js b/email_app/app/actions/cipher-opts_actions.js deleted file mode 100644 index a1b492f..0000000 --- a/email_app/app/actions/cipher-opts_actions.js +++ /dev/null @@ -1,12 +0,0 @@ -import ACTION_TYPES from './actionTypes'; - -export const getCipherOptsHandle = (encType, keyHandle='') => ({ - type: ACTION_TYPES.GET_CIPHER_OPTS_HANDLE, - encType, - keyHandle -}); - -export const deleteCipherOptsHandle = (handleId) => ({ - type: ACTION_TYPES.DELETE_CIPHER_OPTS_HANDLE, - handleId -}); diff --git a/email_app/app/actions/create_account_actions.js b/email_app/app/actions/create_account_actions.js deleted file mode 100644 index 979ab17..0000000 --- a/email_app/app/actions/create_account_actions.js +++ /dev/null @@ -1,10 +0,0 @@ -import ACTION_TYPES from './actionTypes'; - -export const setCreateAccountProcessing = () => ({ - type: ACTION_TYPES.SET_CREATE_ACCOUNT_PROCESSING -}); - -export const setCreateAccountError = (error) => ({ - type: ACTION_TYPES.SET_CREATE_ACCOUNT_ERROR, - error -}); diff --git a/email_app/app/actions/data_id_handle_actions.js b/email_app/app/actions/data_id_handle_actions.js deleted file mode 100644 index 00f751d..0000000 --- a/email_app/app/actions/data_id_handle_actions.js +++ /dev/null @@ -1,29 +0,0 @@ -import ACTION_TYPES from './actionTypes'; -import * as base64 from 'urlsafe-base64'; - -export const getStructuredDataIdHandle = (name, typeTag) => ({ - type: ACTION_TYPES.GET_STRUCTURED_DATA_ID_HANDLE, - typeTag, - name -}); - -export const getAppendableDataIdHandle = (name) => ({ - type: ACTION_TYPES.GET_STRUCTURED_DATA_ID_HANDLE, - isPrivate: true, - name -}); - -export const serialiseDataId = (handleId) => ({ - type: ACTION_TYPES.SERIALISE_DATA_ID, - handleId -}); - -export const deserialiseDataId = (data) => ({ - type: ACTION_TYPES.DESERIALISE_DATA_ID, - data -}); - -export const dropHandler = (handleId) => ({ - type: ACTION_TYPES.DROP_HANDLER, - handleId -}); diff --git a/email_app/app/actions/immutable_data_actions.js b/email_app/app/actions/immutable_data_actions.js deleted file mode 100644 index 1e75ad3..0000000 --- a/email_app/app/actions/immutable_data_actions.js +++ /dev/null @@ -1,40 +0,0 @@ -import ACTION_TYPES from './actionTypes'; - -export const createImmutableDataWriterHandle = () => ({ - type: ACTION_TYPES.CREATE_IMMUT_WRITER_HANDLE -}); - -export const getImmutableDataReadHandle = (handleId) => ({ - type: ACTION_TYPES.GET_IMMUT_READ_HANDLE, - handleId -}); - -export const readImmutableData = (handleId) => ({ - type: ACTION_TYPES.READ_IMMUT_DATA, - handleId -}); - -export const writeImmutableData = (handleId, data) => { - // const dataByteArray = new Uint8Array(new Buffer(JSON.stringify(data))); - return { - type: ACTION_TYPES.WRITE_IMMUT_DATA, - handleId, - data - }; -}; - -export const putImmutableData = (handleId, cipherOptsHandle) => ({ - type: ACTION_TYPES.PUT_IMMUT_DATA, - handleId, - cipherOptsHandle -}); - -export const closeImmutableDataWriter = (handleId) => ({ - type: ACTION_TYPES.CLOSE_IMMUT_DATA_WRITER, - handleId -}); - -export const closeImmutableDataReader = (handleId) => ({ - type: ACTION_TYPES.CLOSE_IMMUT_DATA_READER, - handleId -}); diff --git a/email_app/app/actions/structured_data_actions.js b/email_app/app/actions/structured_data_actions.js deleted file mode 100644 index 1ceba81..0000000 --- a/email_app/app/actions/structured_data_actions.js +++ /dev/null @@ -1,50 +0,0 @@ -import ACTION_TYPES from './actionTypes'; -import { CONSTANTS } from '../constants'; - -export const createStructuredData = (name, data, cipherHandle) => ({ - type: ACTION_TYPES.CREATE_STRUCTURED_DATA, - name, - typeTag: CONSTANTS.TAG_TYPE.DEFAULT, - cipherOpts: cipherHandle, - data - // data: new Buffer(JSON.stringify(data)).toString('base64') -}); - -export const fetchStructuredData = (handleId) => ({ - type: ACTION_TYPES.FETCH_STRUCTURED_DATA, - handleId -}); - -export const fetchStructuredDataHandle = (dataIdHandle) => ({ - type: ACTION_TYPES.FETCH_STRUCTURE_DATA_HANDLE, - dataIdHandle -}); - -export const fetchStructuredDataIdHandle = (handleId) => ({ - type: ACTION_TYPES.FETCH_STRUCTURE_DATA_ID_HANDLE, - handleId -}); - -export const updateStructuredData = (handleId, data, cipherOpts) => ({ - type: ACTION_TYPES.UPDATE_STRUCTURED_DATA, - handleId, - cipherOpts, - data - // data: new Buffer(JSON.stringify(data)).toString('base64') -}); - -export const dropStructuredDataHandle = (handleId) => ({ - type: ACTION_TYPES.DROP_STRUCTURED_DATA_HANDLE, - handleId -}); - -export const postStructuredData = (handleId) => ({ - type: ACTION_TYPES.POST_STRUCTURED_DATA, - handleId -}); - -export const putStructuredData = (handleId) => ({ - type: ACTION_TYPES.PUT_STRUCTURED_DATA, - handleId -}); - diff --git a/email_app/app/components/compose_mail.js b/email_app/app/components/compose_mail.js index 8dd5e35..20d5d9e 100644 --- a/email_app/app/components/compose_mail.js +++ b/email_app/app/components/compose_mail.js @@ -15,42 +15,11 @@ export default class ComposeMail extends Component { this.newMailId = null; this.appendableDataId = null; this.sendMail = this.sendMail.bind(this); - this.handleCancel = this.handleCancel.bind(this); - this.getAppendableDataIdHandle = this.getAppendableDataIdHandle.bind(this); - this.fetchAppendableDataHandle = this.fetchAppendableDataHandle.bind(this); - this.appendAppendableData = this.appendAppendableData.bind(this); - this.dropAppendableData = this.dropAppendableData.bind(this); this.createMail = this.createMail.bind(this); - this.getEncryptedKey = this.getEncryptedKey.bind(this); this.handleTextLimit = this.handleTextLimit.bind(this); } - dropAppendableData() { - const { token, dropAppendableDataHandle, clearMailProcessing } = this.props; - dropAppendableDataHandle(token, this.appendableDataId) - .then(res => { - clearMailProcessing(); - if (res.error) { - return showError('Drop Appendable Handler Error', res.error.message); - } - showSuccess('Mail Success', 'Mail sent successfully!'); - return this.context.router.push('/inbox'); - }); - } - - appendAppendableData(immutableHandle) { - const { token, appendAppendableData, clearMailProcessing } = this.props; - - appendAppendableData(token, this.appendableDataId, immutableHandle) - .then(res => { - if (res.error) { - clearMailProcessing(); - return showError('Append Appendable Data Error', res.error.message); - } - return this.dropAppendableData(); - }); - } createMail(cipherHandleId) { const { token, createImmutableDataWriterHandle, writeImmutableData, putImmutableData, closeImmutableDataWriter, clearMailProcessing } = this.props; @@ -101,78 +70,6 @@ export default class ComposeMail extends Component { createImmutWriter(); } - getEncryptedKey() { - const { token, getEncryptedKey, getCipherOptsHandle, deleteEncryptedKey, clearMailProcessing } = this.props; - - const deleteEncryptHandle = (encryptHandle) => { - deleteEncryptedKey(token, encryptHandle) - .then(res => { - if (res.error) { - return console.error('Delete Encrypt-Key Handle Error', res.error.message); - } - console.warn('Encrypt Key Handle Deleted'); - }); - }; - - const getCipherHandle = (encryptHandle) => { - getCipherOptsHandle(token, CONSTANTS.ENCRYPTION.ASYMMETRIC, encryptHandle) - .then(res => { - if (res.error) { - clearMailProcessing(); - return showError('Get Cipher Opts Handle Error', res.error.message); - } - deleteEncryptHandle(encryptHandle); - return this.createMail(res.payload.data.handleId); - }); - }; - - getEncryptedKey(token, this.appendableDataId) - .then(res => { - if (res.error) { - clearMailProcessing(); - return showError('Get Encrypted Key Error', res.error.message); - } - return getCipherHandle(res.payload.data.handleId); - }); - } - - fetchAppendableDataHandle(dataIdHandle) { - const { token, fetchAppendableDataHandle, dropHandler, clearMailProcessing } = this.props; - - const dropDataIdHandle = () => { - dropHandler(token, dataIdHandle) - .then(res => { - if (res.error) { - return console.error('Drop Data Id Handle Error', res.error.message); - } - console.warn('Data Id Handle Dropped'); - }); - }; - - fetchAppendableDataHandle(token, dataIdHandle) - .then(res => { - if (res.error) { - clearMailProcessing(); - return showError('Fetch Appendable Data Error', res.error.message); - } - this.appendableDataId = res.payload.data.handleId; - dropDataIdHandle(); - return this.getEncryptedKey(); - }); - } - - getAppendableDataIdHandle(emailId) { - const { token, getAppendableDataIdHandle, clearMailProcessing } = this.props; - - getAppendableDataIdHandle(token, base64.encode(hashEmailId(emailId))) - .then(res => { - if (res.error) { - clearMailProcessing(); - return showError('Get Appendable Data Id Handle Error', res.error.message); - } - return this.fetchAppendableDataHandle(res.payload.data.handleId); - }); - } sendMail(e) { const { token, fromMail, setMailProcessing } = this.props; diff --git a/email_app/app/components/create_account.js b/email_app/app/components/create_account.js index f684ade..5921356 100644 --- a/email_app/app/components/create_account.js +++ b/email_app/app/components/create_account.js @@ -17,82 +17,6 @@ export default class CreateAccount extends Component { this.errMrg = null; this.appendableDataHandle = 0; this.handleCreateAccount = this.handleCreateAccount.bind(this); - this.createAppendableData = this.createAppendableData.bind(this); - this.updateStructuredData = this.updateStructuredData.bind(this); - this.dropAppendableData = this.dropAppendableData.bind(this); - } - - dropAppendableData() { - const { token, dropAppendableDataHandle, setCreateAccountError } = this.props; - dropAppendableDataHandle(token, this.appendableDataHandle) - .then(res => { - if (res.error) { - return setCreateAccountError(res.error); - } - console.warn('Dropped Appendable Data Handle'); - }); - } - - updateStructuredData(emailId) { - const { token, rootSDHandle, coreData, updateStructuredData, postStructuredData, setCreateAccountError, getCipherOptsHandle, deleteCipherOptsHandle } = this.props; - coreData.id = emailId; - - const post = () => { - postStructuredData(token, rootSDHandle) - .then((res) => { - if (res.error) { - return setCreateAccountError(res.error); - } - return this.context.router.push('/home'); - }); - }; - - const update = (cipherHandleId) => { - updateStructuredData(token, rootSDHandle, coreData, cipherHandleId) - .then((res) => { - if (res.error) { - return setCreateAccountError(res.error); - } - deleteCipherOptsHandle(token, cipherHandleId); - return post(); - }); - }; - - const getCipherHandle = () => { - getCipherOptsHandle(token, CONSTANTS.ENCRYPTION.SYMMETRIC) - .then(res => { - if (res.error) { - return setCreateAccountError(res.error); - } - return update(res.payload.data.handleId); - }); - }; - - getCipherHandle(); - } - - createAppendableData(emailId) { - const { token, createAppendableData, setCreateAccountError, putAppendableData } = this.props; - const appendableDataName = hashEmailId(emailId); - const put = () => { - putAppendableData(token, this.appendableDataHandle) - .then(res => { - if (res.error) { - return setCreateAccountError(new Error(MESSAGES.EMAIL_ALREADY_TAKEN)); - } - this.dropAppendableData(); - return this.updateStructuredData(emailId); - }); - }; - - createAppendableData(token, appendableDataName) - .then(res => { - if (res.error) { - return setCreateAccountError(new Error(MESSAGES.EMAIL_ALREADY_TAKEN)); - } - this.appendableDataHandle = res.payload.data.handleId; - return put(); - }); } handleCreateAccount(e) { diff --git a/email_app/app/components/initializer.js b/email_app/app/components/initializer.js index 5a9d298..68350b2 100644 --- a/email_app/app/components/initializer.js +++ b/email_app/app/components/initializer.js @@ -1,6 +1,5 @@ import React, { Component, PropTypes } from 'react'; import { remote } from 'electron'; -import { generateStructredDataId } from '../utils/app_utils'; import { CONSTANTS, AUTH_PAYLOAD, MESSAGES } from '../constants'; const showDialog = (title, message) => { @@ -21,11 +20,6 @@ export default class Initializer extends Component { super(); this.createCoreCount = 0; this.getConfiguration = this.getConfiguration.bind(this); - this.writeConfigFile = this.writeConfigFile.bind(this); - this.getStructuredDataHandle = this.getStructuredDataHandle.bind(this); - this.getStructuredDataIdHandle = this.getStructuredDataIdHandle.bind(this); - this.createStructuredData = this.createStructuredData.bind(this); - this.fetchStructuredData = this.fetchStructuredData.bind(this); } componentDidMount() { @@ -34,7 +28,7 @@ export default class Initializer extends Component { .then((app) => { console.log(app); setInitializerTask(MESSAGES.INITIALIZE.CHECK_CONFIGURATION); - return this.getConfiguration(); + return app.auth.refreshContainerAccess().then(() => this.getConfiguration) }) .catch((err) => { console.error(err) @@ -55,114 +49,6 @@ export default class Initializer extends Component { return this.createStructuredData(); } setInitializerTask(MESSAGES.INITIALIZE.FETCH_CORE_STRUCTURE); - return this.getStructuredDataIdHandle(new Buffer(res.payload.data).toString()); - }); - } - - getStructuredDataIdHandle(name) { - const { client, getStructuredDataIdHandle } = this.props; - - getStructuredDataIdHandle(client, name, CONSTANTS.TAG_TYPE.DEFAULT) - .then((res) => { - if (res.error) { - return showDialog('Get Structure Data Handler Error', res.error.message); - } - return this.getStructuredDataHandle(res.payload.data.handleId); - }); - } - - getStructuredDataHandle(handleId) { - const { client, fetchStructuredDataHandle } = this.props; - fetchStructuredDataHandle(client, handleId) - .then(res => { - if (res.error) { - return showDialog('Get Structure Data Handler Error', res.error.message); - } - return this.fetchStructuredData(res.payload.data.handleId); - }); - } - - fetchStructuredData(handleId) { - const { client, fetchStructuredData } = this.props; - fetchStructuredData(client, handleId) - .then(res => { - if (res.error) { - return showDialog('Get Structure Data Error', res.error.message); - } - const emailId = res.payload.data ? res.payload.data.id : null; - if (!emailId) { - return this.context.router.push('/create_account'); - } - return this.context.router.push('/home'); - }); - } - - createStructuredData() { - const { client, createStructuredData, putStructuredData, getCipherOptsHandle, deleteCipherOptsHandle, dropStructuredDataHandle, setInitializerTask } = this.props; - this.createCoreCount++; - const structuredDataId = generateStructredDataId(); - const data = { - id: '', - saved: [], - outbox: [] - }; - - const deleteCipherHandle = (handleId) => { - deleteCipherOptsHandle(client, handleId) - .then((res) => { - if (res.error) { - return showDialog('Delete Cipher Opts Handle Error', res.error.message); - } - console.warn('Deleted Cipher Opts Handle'); - }); - }; - - const put = (handleId) => { - putStructuredData(client, handleId) - .then((res) => { - if (res.error) { - return showDialog('Put Structure Data Error', res.error.message); - } - this.writeConfigFile(structuredDataId); - }); - }; - - const create = (cipherOptsHandle) => { - createStructuredData(client, structuredDataId, data, cipherOptsHandle) - .then((res) => { - if (res.error) { - if (this.createCoreCount > 5) { - return showDialog('Create Core Structure Error', 'Failed to create Core structure'); - } - return this.createStructuredData(); - } - setInitializerTask(MESSAGES.INITIALIZE.WRITE_CONFIG_FILE); - deleteCipherHandle(cipherOptsHandle); - put(res.payload.data.handleId); - }); - }; - - const getCipherHandle = () => { - getCipherOptsHandle(client, CONSTANTS.ENCRYPTION.SYMMETRIC) - .then((res) => { - if (res.error) { - return showDialog('Get Cipher Opts Handle Error', res.error.message); - } - return create(res.payload.data.handleId); - }); - }; - - getCipherHandle(); - } - - writeConfigFile(structuredDataId) { - const { client, writeConfigFile } = this.props; - writeConfigFile(client, structuredDataId) - .then((res) => { - if (res.error) { - return showDialog('Write Configuration File Error', res.error.message); - } - return this.context.router.push('/create_account'); }); } diff --git a/email_app/app/components/mail_inbox.js b/email_app/app/components/mail_inbox.js index daeb7b8..c97b442 100644 --- a/email_app/app/components/mail_inbox.js +++ b/email_app/app/components/mail_inbox.js @@ -13,12 +13,6 @@ export default class MailInbox extends Component { this.refresh = this.refresh.bind(this); this.fetchMails = this.fetchMails.bind(this); this.fetchMail = this.fetchMail.bind(this); - this.iterateAppendableData = this.iterateAppendableData.bind(this); - this.getAppendableDataLength = this.getAppendableDataLength.bind(this); - this.dropAppendableData = this.dropAppendableData.bind(this); - this.getAppendableDataIdHandle = this.getAppendableDataIdHandle.bind(this); - this.fetchAppendableDataHandle = this.fetchAppendableDataHandle.bind(this); - this.fetchAppendableDataMeta = this.fetchAppendableDataMeta.bind(this); this.refresh = this.refresh.bind(this); } @@ -26,142 +20,8 @@ export default class MailInbox extends Component { this.fetchMails(); } - dropAppendableData() { - const { token, dropAppendableDataHandle, clearMailProcessing } = this.props; - dropAppendableDataHandle(token, this.appendableDataHandle) - .then(res => { - clearMailProcessing(); - if (res.error) { - return showError('Drop Appendable data error', res.error.message); - } - }); - } - - getAppendableDataLength() { - const { token, getAppendableDataLength, clearMailProcessing } = this.props; - getAppendableDataLength(token, this.appendableDataHandle) - .then(res => { - if (res.error) { - clearMailProcessing(); - return showError('Get Appendable Data Length Error', res.error.message); - } - return this.dropAppendableData(); - }); - } - - fetchMail(immutableHandleId) { - - const { token, getImmutableDataReadHandle, readImmutableData, closeImmutableDataReader, pushToInbox, clearMailProcessing } = this.props; - - const closeReader = (handleId) => { - closeImmutableDataReader(token, handleId) - .then(res => { - if (res.error) { - return console.error('Close Immutable Data Reader Error', res.error.message); - } - console.warn('Closed Immutable Data Reader'); - }); - }; - - const read = (handleId) => { - readImmutableData(token, handleId) - .then(res => { - if (res.error) { - clearMailProcessing(); - return showError('Read Immutable Data Error', res.error.message); - } - closeReader(handleId); - const data = new Buffer(res.payload.data).toString(); - pushToInbox(JSON.parse(data)); - this.currentIndex++; - if (this.dataLength === this.currentIndex) { - return this.getAppendableDataLength(); - } - return this.iterateAppendableData(); - }); - }; - - const getImmutReader = () => { - console.log('====', immutableHandleId); - getImmutableDataReadHandle(token, immutableHandleId) - .then(res => { - if (res.error) { - clearMailProcessing(); - return showError('Get Immutable Data Reader Error', res.error.message); - } - return read(res.payload.data.handleId); - }); - }; - getImmutReader(); - } - - iterateAppendableData() { - const { token, fetchDataIdAt, clearMailProcessing } = this.props; - fetchDataIdAt(token, this.appendableDataHandle, this.currentIndex) - .then(res => { - if (res.error) { - clearMailProcessing(); - return showError('Fetch Data Id At Error', res.error.message); - } - return this.fetchMail(res.payload.data.handleId); - }); - } - - fetchAppendableDataMeta() { - const { token, fetchAppendableDataMeta, clearMailProcessing } = this.props; - - fetchAppendableDataMeta(token, this.appendableDataHandle) - .then((res) => { - if (res.error) { - clearMailProcessing(); - return showError('Fetch Appendable Data Meta Error', res.error.message); - } - const dataLength = parseInt(res.payload.data.dataLength); - if (dataLength === 0) { - return this.getAppendableDataLength(); - } - this.dataLength = dataLength; - return this.iterateAppendableData(); - }); - } - - fetchAppendableDataHandle(dataIdHandle) { - const { token, fetchAppendableDataHandle, dropHandler, clearMailProcessing } = this.props; - - const dropDataIdHandle = () => { - dropHandler(token, dataIdHandle) - .then((res) => { - if (res.error) { - return console.error(res.error.message); - } - console.warn(res.payload.data); - }); - }; - - fetchAppendableDataHandle(token, dataIdHandle) - .then(res => { - if (res.error) { - clearMailProcessing(); - return showError('Get Appendable Data Handler Error', res.error.message); - } - this.appendableDataHandle = res.payload.data.handleId; - dropDataIdHandle(); - return this.fetchAppendableDataMeta(); - }); - } - - getAppendableDataIdHandle() { - const { token, coreData, getAppendableDataIdHandle, clearMailProcessing } = this.props; - const appendableDataName = hashEmailId(coreData.id); + fetchMail(id) { - getAppendableDataIdHandle(token, appendableDataName) - .then((res) => { - if (res.error) { - clearMailProcessing(); - return showError('Get Appendable Data Handler Error', res.error.message); - } - this.fetchAppendableDataHandle(res.payload.data.handleId); - }); } fetchMails() { diff --git a/email_app/app/components/mail_list.js b/email_app/app/components/mail_list.js index edef594..5dfb5ae 100644 --- a/email_app/app/components/mail_list.js +++ b/email_app/app/components/mail_list.js @@ -19,12 +19,6 @@ export default class MailList extends Component { this.goBack = this.goBack.bind(this); this.handleDelete = this.handleDelete.bind(this); this.handleSave = this.handleSave.bind(this); - this.fetchStructuredData = this.fetchStructuredData.bind(this); - this.getAppendableDataIdHandle = this.getAppendableDataIdHandle.bind(this); - this.fetchAppendableDataHandle = this.fetchAppendableDataHandle.bind(this); - this.removeFromAppendableData = this.removeFromAppendableData.bind(this); - this.dropAppendableDatahandler = this.dropAppendableDatahandler.bind(this); - this.clearDeletedData = this.clearDeletedData.bind(this); } goBack() { @@ -41,200 +35,12 @@ export default class MailList extends Component { } } - fetchStructuredData() { - const { token, rootSDHandle, fetchStructuredData } = this.props; - - if (this.activeType === CONSTANTS.HOME_TABS.INBOX) { - return this.goBack(); - } - - fetchStructuredData(token, rootSDHandle) - .then(res => { - if (res.error) { - return showError('Get Structure Data Error', res.error.message); - } - return this.goBack(); - }); - } - - dropAppendableDatahandler() { - const { token, dropAppendableDataHandle } = this.props; - dropAppendableDataHandle(token, this.appendableHandlerId) - .then(res => { - if (res.error) { - return showError('Drop Appendable data error', res.error.message); - } - return this.fetchStructuredData(); - }); - } - - clearDeletedData() { - const { token, clearDeletedData, postAppendableData } = this.props; - const post = () => { - postAppendableData(token, this.appendableHandlerId) - .then(res => { - if (res.error) { - return showError('Post Appendabel Data Error', res.error.message); - } - return this.dropAppendableDatahandler(); - }); - }; - - clearDeletedData(token, this.appendableHandlerId) - .then(res => { - if (res.error) { - return showError('Clear Appendabel Delete Data Error', res.error.message); - } - return post(); - }); - } - - removeFromAppendableData() { - const { token, removeFromAppendableData, postAppendableData } = this.props; - - const post = () => { - postAppendableData(token, this.appendableHandlerId) - .then(res => { - if (res.error) { - return showError('Post Appendabel Data Error', res.error.message); - } - return this.clearDeletedData(); - }); - }; - - removeFromAppendableData(token, this.appendableHandlerId, this.activeIndex) - .then(res => { - if (res.error) { - return showError('Remove From Appendabel Data Error', res.error.message); - } - return post(); - }); - } - - fetchAppendableDataHandle(dataIdHandle) { - const { token, fetchAppendableDataHandle, dropHandler } = this.props; - - const dropDataIdHandle = () => { - dropHandler(token, dataIdHandle) - .then(res => { - if (res.error) { - return console.error('Drop Appendable Data Id Handler error :: ', res.error.message); - } - console.warn(res.payload.data); - }); - }; - - fetchAppendableDataHandle(token, dataIdHandle) - .then(res => { - if (res.error) { - return showError('Get Appendable Data Handle Error', res.error.message); - } - this.appendableHandlerId = res.payload.data.handleId; - dropDataIdHandle(dataIdHandle); - return this.removeFromAppendableData(); - }); - } - - getAppendableDataIdHandle() { - const { token, coreData, getAppendableDataIdHandle } = this.props; - const appendableDataName = base64.encode(hashEmailId(coreData.id)); - getAppendableDataIdHandle(token, appendableDataName) - .then(res => { - if (res.error) { - return showError('Get Appendable Data Id Handle Error', res.error.message); - } - return this.fetchAppendableDataHandle(res.payload.data.handleId); - }); - } - handleDelete(e) { e.preventDefault(); - const confirmDelete = (res) => { - if (res === 1) { - return; - } - const { token, coreData, rootSDHandle, updateStructuredData, postStructuredData, getCipherOptsHandle, deleteCipherOptsHandle } = this.props; - - this.activeIndex = parseInt(e.target.dataset.index); - - if (this.activeType === CONSTANTS.HOME_TABS.INBOX) { - return this.getAppendableDataIdHandle(); - } - - let saved = []; - saved = coreData.saved.filter((mail, i) => { - if (i !== this.activeIndex) { - return mail; - } - }); - - coreData.saved = saved; - - const post = (cipherHandleId) => { - postStructuredData(token, rootSDHandle) - .then(res => { - if (res.error) { - return showError('Post Structure Data Error', res.error.message); - } - showSuccess('Deleted Mail', 'Mail Deleted Successfully'); - deleteCipherOptsHandle(token, cipherHandleId); - return this.fetchStructuredData(); - }); - }; - - const update = (cipherHandleId) => { - updateStructuredData(token, rootSDHandle, coreData, cipherHandleId) - .then(res => { - if (res.error) { - return showError('Deleting Mail Error', res.error.message); - } - return post(cipherHandleId); - }); - }; - - getCipherOptsHandle(token, CONSTANTS.ENCRYPTION.SYMMETRIC) - .then(res => { - if (res.error) { - return showError('Get Cipher Handle Error', res.error.message); - } - return update(res.payload.data.handleId); - }); - }; - - // remote.dialog.showMessageBox({ - // type: 'warning', - // title: 'Do you want to Delete?', - // message: '', - // buttons: [ 'Ok', "Cancel" ] - // }, confirmDelete); - confirmDelete(0); } handleSave(e) { e.preventDefault(); - this.activeIndex = parseInt(e.target.dataset.index); - const { token, coreData, rootSDHandle, updateStructuredData, postStructuredData } = this.props; - - coreData.saved.unshift(coreData.inbox[this.activeIndex]); - delete coreData.inbox[this.activeIndex]; - - const post = () => { - postStructuredData(token, rootSDHandle) - .then(res => { - if (res.error) { - return showError('Saving Mail Error', res.error.message); - } - return this.getAppendableDataIdHandle(); - }); - }; - - updateStructuredData(token, rootSDHandle, coreData) - .then(res => { - if (res.error) { - return showError('Update Mail Error', res.error.message); - } - return post(); - }); } render() { diff --git a/email_app/app/containers/compose_mail_container.js b/email_app/app/containers/compose_mail_container.js index 1408dd7..0c28f2f 100644 --- a/email_app/app/containers/compose_mail_container.js +++ b/email_app/app/containers/compose_mail_container.js @@ -1,9 +1,5 @@ import { connect } from 'react-redux'; import ComposeMail from '../components/compose_mail'; -import { fetchAppendableDataHandle, appendAppendableData, getEncryptedKey, deleteEncryptedKey, dropAppendableDataHandle } from '../actions/appendable_data_actions'; -import { getCipherOptsHandle, deleteCipherOptsHandle } from '../actions/cipher-opts_actions'; -import { createImmutableDataWriterHandle, writeImmutableData, putImmutableData, closeImmutableDataWriter } from '../actions/immutable_data_actions'; -import { getAppendableDataIdHandle, dropHandler } from '../actions/data_id_handle_actions'; import { cancelCompose, setMailProcessing, clearMailProcessing } from '../actions/mail_actions'; const mapStateToProps = state => { @@ -20,21 +16,6 @@ const mapDispatchToProps = dispatch => { setMailProcessing: _ => (dispatch(setMailProcessing())), clearMailProcessing: _ => (dispatch(clearMailProcessing())), cancelCompose: _ => dispatch(cancelCompose()), - getAppendableDataIdHandle: (token, name) => (dispatch(getAppendableDataIdHandle(token, name))), - dropHandler: (token, id) => (dispatch(dropHandler(token, id))), - fetchAppendableDataHandle: (token, id) => (dispatch(fetchAppendableDataHandle(token, id))), - getEncryptedKey: (token, handleId) => dispatch(getEncryptedKey(token, handleId)), - deleteEncryptedKey: (token, handleId) => dispatch(deleteEncryptedKey(token, handleId)), - getCipherOptsHandle: (token, encType, keyHandle) => (dispatch(getCipherOptsHandle(token, encType, keyHandle))), - deleteCipherOptsHandle: (token, handleId) => (dispatch(deleteCipherOptsHandle(token, handleId))), - createImmutableDataWriterHandle: (token) => (dispatch(createImmutableDataWriterHandle(token))), - writeImmutableData : (token, handleId, data) => (dispatch(writeImmutableData(token, handleId, data))), - putImmutableData: (token, handleId, cipherHandle) => (dispatch(putImmutableData(token, handleId, cipherHandle))), - closeImmutableDataWriter: (token, handleId) => (dispatch(closeImmutableDataWriter(token, handleId))), - appendAppendableData: (token, id, dataId) => ( - dispatch(appendAppendableData(token, id, dataId)) - ), - dropAppendableDataHandle: (token, handleId) => (dispatch(dropAppendableDataHandle(token, handleId))) }; }; diff --git a/email_app/app/containers/create_account_component.js b/email_app/app/containers/create_account_component.js index f69c7a1..bf90e3a 100644 --- a/email_app/app/containers/create_account_component.js +++ b/email_app/app/containers/create_account_component.js @@ -1,9 +1,5 @@ import { connect } from 'react-redux'; import CreateAccount from '../components/create_account'; -import { createAppendableData, dropAppendableDataHandle, putAppendableData } from '../actions/appendable_data_actions'; -import { updateStructuredData, postStructuredData } from '../actions/structured_data_actions'; -import { getCipherOptsHandle, deleteCipherOptsHandle } from '../actions/cipher-opts_actions'; -import { setCreateAccountError, setCreateAccountProcessing } from '../actions/create_account_actions'; const mapStateToProps = state => { return { @@ -18,15 +14,8 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - createAppendableData: (token, name) => (dispatch(createAppendableData(token, name))), - putAppendableData: (token, id) => (dispatch(putAppendableData(token, id))), - updateStructuredData: (token, handleId, data, cipherOpts) => (dispatch(updateStructuredData(token, handleId, data, cipherOpts))), - postStructuredData: (token, handleId) => (dispatch(postStructuredData(token, handleId))), setCreateAccountError: error => (dispatch(setCreateAccountError(error))), setCreateAccountProcessing: () => (dispatch(setCreateAccountProcessing())), - dropAppendableDataHandle: (token, handleId) => (dispatch(dropAppendableDataHandle(token, handleId))), - getCipherOptsHandle: (token, encType, keyHandle) => (dispatch(getCipherOptsHandle(token, encType, keyHandle))), - deleteCipherOptsHandle: (token, handleId) => (dispatch(deleteCipherOptsHandle(token, handleId))) }; }; diff --git a/email_app/app/containers/initializer_container.js b/email_app/app/containers/initializer_container.js index b953e0c..93a6e16 100644 --- a/email_app/app/containers/initializer_container.js +++ b/email_app/app/containers/initializer_container.js @@ -1,10 +1,6 @@ import { connect } from 'react-redux'; import Initializer from '../components/initializer'; import { setInitializerTask, authoriseApplication } from '../actions/initializer_actions'; -import { getConfigFile, writeConfigFile } from '../actions/nfs_actions'; -import { createStructuredData, fetchStructuredData, fetchStructuredDataHandle, putStructuredData, dropStructuredDataHandle } from '../actions/structured_data_actions'; -import { getStructuredDataIdHandle } from '../actions/data_id_handle_actions'; -import { getCipherOptsHandle, deleteCipherOptsHandle } from '../actions/cipher-opts_actions'; const mapStateToProps = state => { return { @@ -19,16 +15,6 @@ const mapDispatchToProps = dispatch => { return { setInitializerTask: task => (dispatch(setInitializerTask(task))), authoriseApplication: payload => (dispatch(authoriseApplication(payload))), - getConfigFile: (client) => (dispatch(getConfigFile(client))), - writeConfigFile: (client, data) => (dispatch(writeConfigFile(client, data))), - getCipherOptsHandle: (client, encType, keyHandle) => (dispatch(getCipherOptsHandle(client, encType, keyHandle))), - deleteCipherOptsHandle: (client, handleId) => (dispatch(deleteCipherOptsHandle(client, handleId))), - getStructuredDataIdHandle: (client, name, typeTag) => (dispatch(getStructuredDataIdHandle(client, name, typeTag))), - createStructuredData: (client, name, data) => (dispatch(createStructuredData(client, name, data))), - fetchStructuredData: (client, handleId) => (dispatch(fetchStructuredData(client, handleId))), - fetchStructuredDataHandle: (client, dataIdHandle) => (dispatch(fetchStructuredDataHandle(client, dataIdHandle))), - putStructuredData: (client, handleId) => (dispatch(putStructuredData(client, handleId))), - dropStructuredDataHandle: (client, handleId) => (dispatch(dropStructuredDataHandle(client, handleId))) }; }; diff --git a/email_app/app/containers/mail_inbox_container.js b/email_app/app/containers/mail_inbox_container.js index 2e68ccc..a3f636c 100644 --- a/email_app/app/containers/mail_inbox_container.js +++ b/email_app/app/containers/mail_inbox_container.js @@ -1,22 +1,6 @@ import { connect } from 'react-redux'; import MailInbox from '../components/mail_inbox'; -import { - fetchAppendableDataMeta, - fetchAppendableDataHandle, - fetchDataIdAt, - removeFromAppendableData, - clearDeleteData, - postAppendableData, - getAppendableDataLength, - dropAppendableDataHandle, - clearDeletedData -} from '../actions/appendable_data_actions'; -import { getImmutableDataReadHandle, readImmutableData, closeImmutableDataReader } from '../actions/immutable_data_actions'; -import { updateStructuredData, fetchStructuredData, postStructuredData } from '../actions/structured_data_actions'; -import { getAppendableDataIdHandle, dropHandler } from '../actions/data_id_handle_actions'; -import { pushToInbox, clearInbox } from '../actions/initializer_actions'; import { setMailProcessing, clearMailProcessing } from '../actions/mail_actions'; -import { getCipherOptsHandle, deleteCipherOptsHandle } from '../actions/cipher-opts_actions'; const mapStateToProps = state => { return { @@ -35,24 +19,6 @@ const mapDispatchToProps = dispatch => { clearMailProcessing: () => (dispatch(clearMailProcessing())), pushToInbox: data => (dispatch(pushToInbox(data))), clearInbox: _ => (dispatch(clearInbox())), - getAppendableDataIdHandle: (token, name) => (dispatch(getAppendableDataIdHandle(token, name))), - dropHandler: (token, handleId) => dispatch(dropHandler(token, handleId)), - fetchAppendableDataHandle: (token, dataIdHandle) => (dispatch(fetchAppendableDataHandle(token, dataIdHandle))), - fetchAppendableDataMeta: (token, handleId) => (dispatch(fetchAppendableDataMeta(token, handleId))), - fetchDataIdAt: (token, handlerId, index) => (dispatch(fetchDataIdAt(token, handlerId, index))), - removeFromAppendableData: (token, handlerId, index) => (dispatch(removeFromAppendableData(token, handlerId, index))), - getAppendableDataLength: (token, handlerId) => (dispatch(getAppendableDataLength(token, handlerId))), - clearDeletedData: (token, handlerId) => (dispatch(clearDeletedData(token, handlerId))), - postAppendableData: (token, handlerId) => (dispatch(postAppendableData(token, handlerId))), - dropAppendableDataHandle: (token, handlerId) => (dispatch(dropAppendableDataHandle(token, handlerId))), - updateStructuredData: (token, handleId, data, cipherOpts) => (dispatch(updateStructuredData(token, handleId, data, cipherOpts))), - postStructuredData: (token, handleId) => (dispatch(postStructuredData(token, handleId))), - fetchStructuredData: (token, handleId) => (dispatch(fetchStructuredData(token, handleId))), - getImmutableDataReadHandle: (token, handleId) => (dispatch(getImmutableDataReadHandle(token, handleId))), - readImmutableData: (token, handleId) => (dispatch(readImmutableData(token, handleId))), - closeImmutableDataReader: (token, handleId) => (dispatch(closeImmutableDataReader(token, handleId))), - getCipherOptsHandle: (token, encType, keyHandle) => (dispatch(getCipherOptsHandle(token, encType, keyHandle))), - deleteCipherOptsHandle: (token, handleId) => (dispatch(deleteCipherOptsHandle(token, handleId))) }; }; export default connect(mapStateToProps, mapDispatchToProps)(MailInbox); diff --git a/email_app/app/containers/mail_saved_container.js b/email_app/app/containers/mail_saved_container.js index 1b4b3b4..08b45df 100644 --- a/email_app/app/containers/mail_saved_container.js +++ b/email_app/app/containers/mail_saved_container.js @@ -1,10 +1,6 @@ import { connect } from 'react-redux'; import MailSaved from '../components/mail_saved'; -import { updateStructuredData, fetchStructuredData, postStructuredData } from '../actions/structured_data_actions'; import { setMailProcessing } from '../actions/mail_actions'; -import { getAppendableDataIdHandle, dropHandler } from '../actions/data_id_handle_actions'; -import { fetchAppendableDataHandle, removeFromAppendableData, clearDeletedData, dropAppendableDataHandle } from '../actions/appendable_data_actions'; -import { getCipherOptsHandle, deleteCipherOptsHandle } from '../actions/cipher-opts_actions'; const mapStateToProps = state => { return { @@ -19,17 +15,6 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { setMailProcessing: () => (dispatch(setMailProcessing())), - getAppendableDataIdHandle: (token, name) => (dispatch(getAppendableDataIdHandle(token, name))), - dropHandler: (token, handleId) => (dispatch(dropHandler(token, handleId))), - fetchAppendableDataHandle: (token, dataHandleId) => (dispatch(fetchAppendableDataHandle(token, dataHandleId))), - removeFromAppendableData: (token, handleId, index) => (dispatch(removeFromAppendableData(token, handleId, index))), - dropAppendableDataHandle: (token, handlerId) => (dispatch(dropAppendableDataHandle(token, handlerId))), - clearDeletedData: (token, handlerId) => (dispatch(clearDeletedData(token, handlerId))), - updateStructuredData: (token, handleId, data, cipherOpts) => (dispatch(updateStructuredData(token, handleId, data, cipherOpts))), - fetchStructuredData: (token, handleId) => (dispatch(fetchStructuredData(token, handleId))), - postStructuredData: (token, handleId) => (dispatch(postStructuredData(token, handleId))), - getCipherOptsHandle: (token, encType, keyHandle) => (dispatch(getCipherOptsHandle(token, encType, keyHandle))), - deleteCipherOptsHandle: (token, handleId) => (dispatch(deleteCipherOptsHandle(token, handleId))) }; }; diff --git a/email_app/app/utils/app_utils.js b/email_app/app/utils/app_utils.js index cf684bd..63c8499 100644 --- a/email_app/app/utils/app_utils.js +++ b/email_app/app/utils/app_utils.js @@ -28,10 +28,6 @@ export const hashEmailId = emailId => { return crypto.createHash('sha256').update(emailId).digest('base64'); }; -export const generateStructredDataId = () => { - return base64.encode(crypto.randomBytes(32).toString('base64')); -}; - export const showError = (title, errMsg) => { remote.dialog.showMessageBox({ type: 'error', From f0e34896d6cddc63e2da8e572035a331fccb49e6 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Mon, 27 Mar 2017 13:13:01 +0200 Subject: [PATCH 08/44] feat/Email Load accounts from new HomeContainer --- email_app/app/actions/actionTypes.js | 43 +++---------------- email_app/app/actions/initializer_actions.js | 19 +++++--- email_app/app/components/initializer.js | 24 +++-------- .../app/containers/initializer_container.js | 3 +- email_app/app/reducers/initialiser.js | 26 ++--------- 5 files changed, 28 insertions(+), 87 deletions(-) diff --git a/email_app/app/actions/actionTypes.js b/email_app/app/actions/actionTypes.js index b4ff750..ef9fb7e 100644 --- a/email_app/app/actions/actionTypes.js +++ b/email_app/app/actions/actionTypes.js @@ -1,49 +1,18 @@ const ACTION_TYPES = { AUTHORISE_APP: 'AUTHORISE_APP', GET_CONFIG_FILE: 'GET_CONFIG_FILE', - WRITE_CONFIG_FILE: 'WRITE_CONFIG_FILE', - FETCH_DATA_ID_AT: 'FETCH_DATA_ID_AT', + REFRESH_EMAIL: 'REFRESH_EMAIL', + SET_INITIALIZER_TASK: 'SET_INITIALIZER_TASK', + + // legacy SET_CREATE_ACCOUNT_PROCESSING: 'SET_CREATE_ACCOUNT_PROCESSING', SET_CREATE_ACCOUNT_ERROR: 'SET_CREATE_ACCOUNT_ERROR', - CREATE_STRUCTURED_DATA: 'CREATE_STRUCTURED_DATA', - UPDATE_STRUCTURED_DATA: 'UPDATE_STRUCTURED_DATA', - FETCH_STRUCTURED_DATA: 'FETCH_STRUCTURED_DATA', - FETCH_STRUCTURE_DATA_HANDLE: 'FETCH_STRUCTURE_DATA_HANDLE', - DROP_STRUCTURED_DATA_HANDLE: 'DROP_STRUCTURED_DATA_HANDLE', - FETCH_STRUCTURE_DATA_ID_HANDLE: 'FETCH_STRUCTURE_DATA_ID_HANDLE', - POST_STRUCTURED_DATA: 'POST_STRUCTURED_DATA', - PUT_STRUCTURED_DATA: 'PUT_STRUCTURED_DATA', - SET_INITIALIZER_TASK: 'SET_INITIALIZER_TASK', - SET_APPENDABLE_DATA_ID: 'SET_APPENDABLE_DATA_ID', - CREATE_APPENDABLE_DATA: 'CREATE_APPENDABLE_DATA', - FETCH_APPENDABLE_DATA_META: 'FETCH_APPENDABLE_DATA_META', - FETCH_APPENDABLE_DATA_HANDLER: 'FETCH_APPENDABLE_DATA_HANDLER', - APPEND_APPENDABLE_DATA: 'APPEND_APPENDABLE_DATA', - REMOVE_FROM_APPENDABLE_DATA: 'REMOVE_FROM_APPENDABLE_DATA', - POST_APPENDABLE_DATA: 'POST_APPENDABLE_DATA', - PUT_APPENDABLE_DATA: 'PUT_APPENDABLE_DATA', - DROP_APPENDABLE_DATA_HANDLE: 'DROP_APPENDABLE_DATA_HANDLE', - CLEAR_DELETE_DATA: 'CLEAR_DELETE_DATA', - GET_APPENDABLE_DATA_LENGTH: 'GET_APPENDABLE_DATA_LENGTH', - CREATE_IMMUT_WRITER_HANDLE: 'CREATE_IMMUT_WRITER_HANDLE', - GET_IMMUT_READ_HANDLE: 'GET_IMMUT_READ_HANDLE', - CLOSE_IMMUT_DATA_READER: 'CLOSE_IMMUT_DATA_READER', - READ_IMMUT_DATA: 'READ_IMMUT_DATA', - WRITE_IMMUT_DATA: 'WRITE_IMMUT_DATA', - CLOSE_IMMUT_DATA_WRITER: 'CLOSE_IMMUT_DATA_WRITER', - PUT_IMMUT_DATA: 'PUT_IMMUT_DATA', + PUSH_MAIL: 'PUSH_MAIL', SET_MAIL_PROCESSING: 'SET_MAIL_PROCESSING', CLEAR_MAIL_PROCESSING: 'CLEAR_MAIL_PROCESSING', SET_ACTIVE_MAIL: 'SET_ACTIVE_MAIL', - SERIALISE_DATA_ID: 'SERIALISE_DATA_ID', - DESERIALISE_DATA_ID: 'DESERIALISE_DATA_ID', - GET_STRUCTURED_DATA_ID_HANDLE: 'GET_STRUCTURED_DATA_ID_HANDLE', - DROP_HANDLER: 'DROP_HANDLER', - GET_ENCRYPTED_KEY: 'GET_ENCRYPTED_KEY', - DELETE_ENCRYPTED_KEY: 'DELETE_ENCRYPTED_KEY', - GET_CIPHER_OPTS_HANDLE: 'GET_CIPHER_OPTS_HANDLE', - DELETE_CIPHER_OPTS_HANDLE: 'DELETE_CIPHER_OPTS_HANDLE', + CANCEL_COMPOSE: 'CANCEL_COMPOSE', CLEAR_INBOX: 'CLEAR_INBOX', PUSH_TO_INBOX: 'PUSH_TO_INBOX' diff --git a/email_app/app/actions/initializer_actions.js b/email_app/app/actions/initializer_actions.js index 49dc94f..aeb04cd 100644 --- a/email_app/app/actions/initializer_actions.js +++ b/email_app/app/actions/initializer_actions.js @@ -41,11 +41,16 @@ export const authoriseApplication = (appData, permissions) => { }; }; -export const pushToInbox = (data) => ({ - type: ACTION_TYPES.PUSH_TO_INBOX, - data -}); +export const refreshConfig = (app) => { + const accounts = {}; + return { + type: ACTION_TYPES.REFRESH_EMAIL, + payload: app.auth.getHomeContainer() + .then((mdata) => mdata.getEntries() + .then((entries) => entries.forEach((name, valV) => { + accounts[name.toString()] = valV + })) + .then(() => accounts)) + }}; + -export const clearInbox = _ => ({ - type: ACTION_TYPES.CLEAR_INBOX -}); diff --git a/email_app/app/components/initializer.js b/email_app/app/components/initializer.js index 68350b2..057139c 100644 --- a/email_app/app/components/initializer.js +++ b/email_app/app/components/initializer.js @@ -19,16 +19,17 @@ export default class Initializer extends Component { constructor() { super(); this.createCoreCount = 0; - this.getConfiguration = this.getConfiguration.bind(this); } componentDidMount() { const { authoriseApplication, setInitializerTask } = this.props; authoriseApplication(AUTH_PAYLOAD, {"_publicNames" : ["Insert"]}) - .then((app) => { - console.log(app); + .then((res) => { + const app = res.value; setInitializerTask(MESSAGES.INITIALIZE.CHECK_CONFIGURATION); - return app.auth.refreshContainerAccess().then(() => this.getConfiguration) + return app.auth.refreshContainerAccess() + .then(() => this.props.refreshConfig(app)) + // .then(() => setInitializerTask(MESSAGES.)) }) .catch((err) => { console.error(err) @@ -36,21 +37,6 @@ export default class Initializer extends Component { }); } - getConfiguration() { - const { client, getConfigFile, setInitializerTask } = this.props; - if (!client) { - throw new Error('Application client not found.'); - } - - getConfigFile(client) - .then(res => { - if (res.error) { - setInitializerTask(MESSAGES.INITIALIZE.CREATE_CORE_STRUCTURE); - return this.createStructuredData(); - } - setInitializerTask(MESSAGES.INITIALIZE.FETCH_CORE_STRUCTURE); - }); - } render() { const { tasks } = this.props; diff --git a/email_app/app/containers/initializer_container.js b/email_app/app/containers/initializer_container.js index 93a6e16..a54375f 100644 --- a/email_app/app/containers/initializer_container.js +++ b/email_app/app/containers/initializer_container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import Initializer from '../components/initializer'; -import { setInitializerTask, authoriseApplication } from '../actions/initializer_actions'; +import { setInitializerTask, authoriseApplication, refreshConfig } from '../actions/initializer_actions'; const mapStateToProps = state => { return { @@ -15,6 +15,7 @@ const mapDispatchToProps = dispatch => { return { setInitializerTask: task => (dispatch(setInitializerTask(task))), authoriseApplication: payload => (dispatch(authoriseApplication(payload))), + refreshConfig: mdata => (dispatch(refreshConfig(mdata))) }; }; diff --git a/email_app/app/reducers/initialiser.js b/email_app/app/reducers/initialiser.js index 7901b1f..48abdf2 100644 --- a/email_app/app/reducers/initialiser.js +++ b/email_app/app/reducers/initialiser.js @@ -4,6 +4,7 @@ import { MESSAGES } from '../constants'; const initialState = { client: '', tasks: [], + account: null, config: null, coreData: { id: '', @@ -34,29 +35,8 @@ const initializer = (state = initialState, action) => { case `${ACTION_TYPES.GET_CONFIG_FILE}_SUCCESS`: return { ...state, config: action.payload.data }; break; - case `${ACTION_TYPES.FETCH_STRUCTURE_DATA_HANDLE}_SUCCESS`: - return { ...state, rootSDHandle: action.payload.data.handleId }; - break; - case `${ACTION_TYPES.CREATE_STRUCTURED_DATA}_SUCCESS`: - return { ...state, rootSDHandle: action.payload.data.handleId }; - break; - case `${ACTION_TYPES.GET_APPENDABLE_DATA_LENGTH}_SUCCESS`: { - const inboxSize = ((new Buffer(action.payload.data)).length / 1024).toFixed(2); - return { ...state, inboxSize } - } - case `${ACTION_TYPES.FETCH_STRUCTURED_DATA}_SUCCESS`: - if (!action.payload.data) { - return state; - } - return { - ...state, - coreData: { - ...state.coreData, - id: action.payload.data.id, - saved: action.payload.data.saved.slice(), - outbox: action.payload.data.outbox.slice() - } - }; + case `${ACTION_TYPES.REFRESH_EMAILS}_SUCCESS`: + return { ...state, accounts: action.payload.data }; break; case ACTION_TYPES.PUSH_TO_INBOX: { const inbox = state.coreData.inbox.slice(); From a569d971bd140da64ca5d9428753fd57799c66fb Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Mon, 27 Mar 2017 13:13:40 +0200 Subject: [PATCH 09/44] docs/Readme Update Documentation on using electron forge --- email_app/README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/email_app/README.md b/email_app/README.md index 25483cd..d99e3fa 100644 --- a/email_app/README.md +++ b/email_app/README.md @@ -20,17 +20,18 @@ $ cd your-project-name && npm install ## Run -Run this two commands __simultaneously__ in different console tabs. - ```bash -$ npm run hot-server -$ npm run start-hot +$ npm start ``` -or run two servers with one command +This starts the app in development mode with hot-reloading. + +### Faking Authentication + +If you don't have authenticator set up and want to run the test with randomly generated testing credentials, run it as: ```bash -$ npm run dev +$ NODE_ENV=development SAFE_FAKE_AUTH=1 npm start ``` *Note: requires a node version >= 4 and an npm version >= 2.* From 6f90c15db7d0632476f2444137a01d3cc203016f Mon Sep 17 00:00:00 2001 From: bochaco Date: Fri, 31 Mar 2017 18:45:31 -0300 Subject: [PATCH 10/44] First draft to handle authentication --- email_app/app/actions/actionTypes.js | 4 +- email_app/app/actions/initializer_actions.js | 62 +++++++++++-------- email_app/app/components/create_account.js | 7 ++- email_app/app/components/initializer.js | 33 +++++++--- email_app/app/constants.js | 7 +-- .../containers/create_account_component.js | 5 +- .../app/containers/initializer_container.js | 4 +- email_app/app/reducers/initialiser.js | 22 ++++--- 8 files changed, 89 insertions(+), 55 deletions(-) diff --git a/email_app/app/actions/actionTypes.js b/email_app/app/actions/actionTypes.js index ef9fb7e..830a60c 100644 --- a/email_app/app/actions/actionTypes.js +++ b/email_app/app/actions/actionTypes.js @@ -1,6 +1,6 @@ const ACTION_TYPES = { AUTHORISE_APP: 'AUTHORISE_APP', - GET_CONFIG_FILE: 'GET_CONFIG_FILE', + GET_CONFIG: 'GET_CONFIG', REFRESH_EMAIL: 'REFRESH_EMAIL', SET_INITIALIZER_TASK: 'SET_INITIALIZER_TASK', @@ -12,7 +12,7 @@ const ACTION_TYPES = { SET_MAIL_PROCESSING: 'SET_MAIL_PROCESSING', CLEAR_MAIL_PROCESSING: 'CLEAR_MAIL_PROCESSING', SET_ACTIVE_MAIL: 'SET_ACTIVE_MAIL', - + CANCEL_COMPOSE: 'CANCEL_COMPOSE', CLEAR_INBOX: 'CLEAR_INBOX', PUSH_TO_INBOX: 'PUSH_TO_INBOX' diff --git a/email_app/app/actions/initializer_actions.js b/email_app/app/actions/initializer_actions.js index aeb04cd..63c5e34 100644 --- a/email_app/app/actions/initializer_actions.js +++ b/email_app/app/actions/initializer_actions.js @@ -1,12 +1,10 @@ - import { initializeApp, fromAuthURI } from 'safe-app'; import ACTION_TYPES from './actionTypes'; - var authResolver; var authRejecter; -const authPromise = new Promise((resolve, reject) => { +const authPromise = () => new Promise((resolve, reject) => { authResolver = resolve; authRejecter = reject; }); @@ -25,32 +23,46 @@ export const receiveResponse = (uri) => { }; -export const authoriseApplication = (appData, permissions) => { - initializeApp(appData) - .then((app) => - process.env.SAFE_FAKE_AUTH - ? app.auth.loginForTest(permissions) - .then(authResolver) - : app.auth.genAuthUri({}) - .then(resp => app.auth.openUri(resp.uri)) - ).catch(authRejecter) +export const authoriseApplication = (appInfo, permissions, opts) => { + + return function (dispatch) { + dispatch({ + type: ACTION_TYPES.AUTHORISE_APP, + payload: authPromise() + }); + + return initializeApp(appInfo) + .then((app) => + process.env.SAFE_FAKE_AUTH + ? app.auth.loginForTest(permissions, opts) + .then(app => authResolver(app)) + : app.auth.genAuthUri(permissions, opts) + .then(resp => app.auth.openUri(resp.uri)) + ).catch(authRejecter); - return { - type: ACTION_TYPES.AUTHORISE_APP, - payload: authPromise }; }; export const refreshConfig = (app) => { - const accounts = {}; - return { - type: ACTION_TYPES.REFRESH_EMAIL, - payload: app.auth.getHomeContainer() - .then((mdata) => mdata.getEntries() - .then((entries) => entries.forEach((name, valV) => { - accounts[name.toString()] = valV - })) - .then(() => accounts)) - }}; + return function (dispatch) { + dispatch({ + type: ACTION_TYPES.GET_CONFIG, + payload: authPromise() + }); + let accounts = {}; + return app.auth.refreshContainerAccess() + .then(() => app.auth.getHomeContainer()) + .then((mdata) => mdata.getEntries() + .then((entries) => entries.forEach((name, valV) => { + accounts[name.toString()] = valV.buf.toString(); + }) + .then(() => { +// accounts = {myid: "asasasa"}; + return authResolver(accounts); + }) + ) + ).catch(authRejecter); + }; +}; diff --git a/email_app/app/components/create_account.js b/email_app/app/components/create_account.js index 5921356..4ee4a14 100644 --- a/email_app/app/components/create_account.js +++ b/email_app/app/components/create_account.js @@ -21,15 +21,16 @@ export default class CreateAccount extends Component { handleCreateAccount(e) { e.preventDefault(); + const { createAccount, createAccountError } = this.props; const emailId = this.emailId.value; if (!emailId.trim()) { return; } if (emailId.length > CONSTANTS.NEW_EMAIL_SIZE) { - return this.props.setCreateAccountError(new Error(MESSAGES.EMAIL_TOO_LONG)); + return createAccountError(new Error(MESSAGES.EMAIL_TOO_LONG)); } - this.props.setCreateAccountProcessing(); - return this.createAppendableData(emailId); + return createAccount(emailId) + .then(() => this.context.router.push('/home')); } render() { diff --git a/email_app/app/components/initializer.js b/email_app/app/components/initializer.js index 057139c..f29c906 100644 --- a/email_app/app/components/initializer.js +++ b/email_app/app/components/initializer.js @@ -18,25 +18,38 @@ export default class Initializer extends Component { constructor() { super(); - this.createCoreCount = 0; + this.checkConfiguration = this.checkConfiguration.bind(this); } componentDidMount() { - const { authoriseApplication, setInitializerTask } = this.props; - authoriseApplication(AUTH_PAYLOAD, {"_publicNames" : ["Insert"]}) - .then((res) => { - const app = res.value; - setInitializerTask(MESSAGES.INITIALIZE.CHECK_CONFIGURATION); - return app.auth.refreshContainerAccess() - .then(() => this.props.refreshConfig(app)) - // .then(() => setInitializerTask(MESSAGES.)) - }) + this.props.authoriseApplication(AUTH_PAYLOAD, {"_publicNames" : ["Insert"]}) + .then((_) => this.checkConfiguration()) .catch((err) => { console.error(err) return showDialog('Authorisation Error', MESSAGES.AUTHORISATION_ERROR); }); } + checkConfiguration() { + const { client, refreshConfig } = this.props; + if (!client) { + throw new Error('Application client not found.'); + } + + return refreshConfig(client) + .then((_) => { + if (Object.keys(this.props.accounts).length > 0) { + return this.context.router.push('/home'); + } else { + return this.context.router.push('/create_account'); + } + }) + .catch((err) => { + console.error(err) + return showDialog('Error fetching configuration', MESSAGES.CHECK_CONFIGURATION_ERROR); + }); + } + render() { const { tasks } = this.props; diff --git a/email_app/app/constants.js b/email_app/app/constants.js index 8bd93bc..843cc1f 100644 --- a/email_app/app/constants.js +++ b/email_app/app/constants.js @@ -2,10 +2,6 @@ import pkg from '../package.json'; export const CONSTANTS = { LOCAL_AUTH_DATA_KEY: 'local_auth_data_key', - TAG_TYPE: { - DEFAULT: 500, - VERSIONED: 501 - }, APPENDABLE_DATA_FILTER_TYPE: { WHITE_LIST: 'WHITE_LIST', BLACK_LIST: 'BLACK_LIST' @@ -36,7 +32,8 @@ export const MESSAGES = { }, EMAIL_ALREADY_TAKEN: 'Email already taken. Please try again', EMAIL_TOO_LONG: 'EMAIL is too long', - AUTHORISATION_ERROR: 'Failed to authorise with launcher' + AUTHORISATION_ERROR: 'Failed to authorise', + CHECK_CONFIGURATION_ERROR: 'Failed to retrieve configuration' }; export const AUTH_PAYLOAD = { diff --git a/email_app/app/containers/create_account_component.js b/email_app/app/containers/create_account_component.js index bf90e3a..f50173b 100644 --- a/email_app/app/containers/create_account_component.js +++ b/email_app/app/containers/create_account_component.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; import CreateAccount from '../components/create_account'; +import { createAccount, createAccountError } from '../actions/create_account_actions'; const mapStateToProps = state => { return { @@ -14,8 +15,8 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - setCreateAccountError: error => (dispatch(setCreateAccountError(error))), - setCreateAccountProcessing: () => (dispatch(setCreateAccountProcessing())), + createAccountError: error => (dispatch(createAccountError(error))), + createAccount: () => (dispatch(createAccount())), }; }; diff --git a/email_app/app/containers/initializer_container.js b/email_app/app/containers/initializer_container.js index a54375f..f73baa2 100644 --- a/email_app/app/containers/initializer_container.js +++ b/email_app/app/containers/initializer_container.js @@ -5,6 +5,7 @@ import { setInitializerTask, authoriseApplication, refreshConfig } from '../acti const mapStateToProps = state => { return { client: state.initializer.client, + accounts: state.initializer.accounts, tasks: state.initializer.tasks, config: state.initializer.config, coreData: state.initializer.coreData @@ -14,7 +15,8 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { setInitializerTask: task => (dispatch(setInitializerTask(task))), - authoriseApplication: payload => (dispatch(authoriseApplication(payload))), + authoriseApplication: (appInfo, permissions, opts) => + (dispatch(authoriseApplication(appInfo, permissions, opts))), refreshConfig: mdata => (dispatch(refreshConfig(mdata))) }; }; diff --git a/email_app/app/reducers/initialiser.js b/email_app/app/reducers/initialiser.js index 48abdf2..fc60710 100644 --- a/email_app/app/reducers/initialiser.js +++ b/email_app/app/reducers/initialiser.js @@ -4,7 +4,7 @@ import { MESSAGES } from '../constants'; const initialState = { client: '', tasks: [], - account: null, + accounts: null, config: null, coreData: { id: '', @@ -18,7 +18,13 @@ const initialState = { const initializer = (state = initialState, action) => { switch (action.type) { - case ACTION_TYPES.AUTHORISE_APP: { + case ACTION_TYPES.SET_INITIALIZER_TASK: { + const tasks = state.tasks.slice(); + tasks.push(action.task); + return { ...state, tasks }; + break; + } + case `${ACTION_TYPES.AUTHORISE_APP}_LOADING`: { const tasks = state.tasks.slice(); tasks.push(MESSAGES.INITIALIZE.AUTHORISE_APP); return { ...state, tasks }; @@ -27,15 +33,17 @@ const initializer = (state = initialState, action) => { case `${ACTION_TYPES.AUTHORISE_APP}_SUCCESS`: return { ...state, client: action.payload }; break; - case ACTION_TYPES.SET_INITIALIZER_TASK: + case `${ACTION_TYPES.GET_CONFIG}_LOADING`: { const tasks = state.tasks.slice(); - tasks.push(action.task); + tasks.push(MESSAGES.INITIALIZE.CHECK_CONFIGURATION); return { ...state, tasks }; break; - case `${ACTION_TYPES.GET_CONFIG_FILE}_SUCCESS`: - return { ...state, config: action.payload.data }; + } + case `${ACTION_TYPES.GET_CONFIG}_SUCCESS`: + return { ...state, accounts: action.payload }; break; - case `${ACTION_TYPES.REFRESH_EMAILS}_SUCCESS`: + case `${ACTION_TYPES.REFRESH_EMAIL}_SUCCESS`: + console.log("refresh email success", action, state); return { ...state, accounts: action.payload.data }; break; case ACTION_TYPES.PUSH_TO_INBOX: { From eeaec0c7c583458c7440535a115ef4532a8c15b7 Mon Sep 17 00:00:00 2001 From: bochaco Date: Mon, 3 Apr 2017 18:35:04 -0300 Subject: [PATCH 11/44] feat/Enabling the flow for the creation of an email account --- email_app/app/actions/actionTypes.js | 4 +-- .../app/actions/create_account_actions.js | 32 +++++++++++++++++++ email_app/app/actions/initializer_actions.js | 11 +++---- email_app/app/actions/mail_actions.js | 12 ++++--- email_app/app/components/create_account.js | 9 ++---- email_app/app/components/initializer.js | 6 ++-- email_app/app/components/mail_inbox.js | 9 +++--- email_app/app/constants.js | 11 +++---- ...mponent.js => create_account_container.js} | 7 +--- .../app/containers/initializer_container.js | 4 +-- .../app/containers/mail_inbox_container.js | 12 +++---- .../app/containers/mail_saved_container.js | 6 ++-- email_app/app/reducers/create_account.js | 13 ++++++-- email_app/app/reducers/initialiser.js | 14 ++++---- email_app/app/reducers/mail.js | 18 +++++------ email_app/app/routes.js | 2 +- 16 files changed, 99 insertions(+), 71 deletions(-) create mode 100644 email_app/app/actions/create_account_actions.js rename email_app/app/containers/{create_account_component.js => create_account_container.js} (63%) diff --git a/email_app/app/actions/actionTypes.js b/email_app/app/actions/actionTypes.js index 830a60c..eac28c9 100644 --- a/email_app/app/actions/actionTypes.js +++ b/email_app/app/actions/actionTypes.js @@ -5,8 +5,8 @@ const ACTION_TYPES = { SET_INITIALIZER_TASK: 'SET_INITIALIZER_TASK', // legacy - SET_CREATE_ACCOUNT_PROCESSING: 'SET_CREATE_ACCOUNT_PROCESSING', - SET_CREATE_ACCOUNT_ERROR: 'SET_CREATE_ACCOUNT_ERROR', + CREATE_ACCOUNT: 'CREATE_ACCOUNT', + CREATE_ACCOUNT_ERROR: 'SET_CREATE_ACCOUNT_ERROR', PUSH_MAIL: 'PUSH_MAIL', SET_MAIL_PROCESSING: 'SET_MAIL_PROCESSING', diff --git a/email_app/app/actions/create_account_actions.js b/email_app/app/actions/create_account_actions.js new file mode 100644 index 0000000..8569023 --- /dev/null +++ b/email_app/app/actions/create_account_actions.js @@ -0,0 +1,32 @@ +import { CONSTANTS } from '../constants'; +import ACTION_TYPES from './actionTypes'; + +var accountResolver; +var accountRejecter; +const accountPromise = new Promise((resolve, reject) => { + accountResolver = resolve; + accountRejecter = reject; +}); + +export const createAccount = (emailId) => { + return function (dispatch, getState) { + dispatch({ + type: ACTION_TYPES.CREATE_ACCOUNT, + payload: accountPromise + }); + + // FIXME: store private key for encryption in app's container mapped to emailId + + let app = getState().initializer.app; + return app.mutableData.newRandomPublic(CONSTANTS.INBOX_TAG_TYPE) + .then((md) => md.quickSetup({})) + // FIXME: map this address to emailId in publicNames + .then((md) => accountResolver(md)) + .catch(accountRejecter); + }; +}; + +export const createAccountError = (error) => ({ + type: ACTION_TYPES.CREATE_ACCOUNT_ERROR, + error +}); diff --git a/email_app/app/actions/initializer_actions.js b/email_app/app/actions/initializer_actions.js index 63c5e34..a2da244 100644 --- a/email_app/app/actions/initializer_actions.js +++ b/email_app/app/actions/initializer_actions.js @@ -20,7 +20,6 @@ export const receiveResponse = (uri) => { payload: fromAuthURI(uri) .then((app) => authResolver ? authResolver(app) : app) } - }; export const authoriseApplication = (appInfo, permissions, opts) => { @@ -43,25 +42,23 @@ export const authoriseApplication = (appInfo, permissions, opts) => { }; }; -export const refreshConfig = (app) => { +export const refreshConfig = () => { - return function (dispatch) { + return function (dispatch, getState) { dispatch({ type: ACTION_TYPES.GET_CONFIG, payload: authPromise() }); let accounts = {}; + let app = getState().initializer.app; return app.auth.refreshContainerAccess() .then(() => app.auth.getHomeContainer()) .then((mdata) => mdata.getEntries() .then((entries) => entries.forEach((name, valV) => { accounts[name.toString()] = valV.buf.toString(); }) - .then(() => { -// accounts = {myid: "asasasa"}; - return authResolver(accounts); - }) + .then(() => authResolver(accounts)) ) ).catch(authRejecter); }; diff --git a/email_app/app/actions/mail_actions.js b/email_app/app/actions/mail_actions.js index 19e1b32..e992c3e 100644 --- a/email_app/app/actions/mail_actions.js +++ b/email_app/app/actions/mail_actions.js @@ -4,15 +4,19 @@ export const setMailProcessing = () => ({ type: ACTION_TYPES.SET_MAIL_PROCESSING }); +export const clearMailProcessing= _ => ({ + type: ACTION_TYPES.CLEAR_MAIL_PROCESSING +}); + export const setActiveMail = (data) => ({ type: ACTION_TYPES.SET_ACTIVE_MAIL, data }); -export const cancelCompose = () => ({ - type: ACTION_TYPES.CANCEL_COMPOSE +export const clearInbox = () => ({ + type: ACTION_TYPES.SET_ACTIVE_MAIL }); -export const clearMailProcessing= _ => ({ - type: ACTION_TYPES.CLEAR_MAIL_PROCESSING +export const cancelCompose = () => ({ + type: ACTION_TYPES.CANCEL_COMPOSE }); diff --git a/email_app/app/components/create_account.js b/email_app/app/components/create_account.js index 4ee4a14..16dcbf8 100644 --- a/email_app/app/components/create_account.js +++ b/email_app/app/components/create_account.js @@ -4,8 +4,6 @@ import { MESSAGES, CONSTANTS } from '../constants'; export default class CreateAccount extends Component { static propTypes = { - authorised: PropTypes.bool.isRequired, - processing: PropTypes.bool.isRequired, }; static contextTypes = { @@ -15,7 +13,6 @@ export default class CreateAccount extends Component { constructor() { super(); this.errMrg = null; - this.appendableDataHandle = 0; this.handleCreateAccount = this.handleCreateAccount.bind(this); } @@ -26,8 +23,8 @@ export default class CreateAccount extends Component { if (!emailId.trim()) { return; } - if (emailId.length > CONSTANTS.NEW_EMAIL_SIZE) { - return createAccountError(new Error(MESSAGES.EMAIL_TOO_LONG)); + if (emailId.length > CONSTANTS.EMAIL_ID_MAX_LENGTH) { + return createAccountError(new Error(MESSAGES.EMAIL_ID_TOO_LONG)); } return createAccount(emailId) .then(() => this.context.router.push('/home')); @@ -45,7 +42,7 @@ export default class CreateAccount extends Component {
{this.emailId = c;}} autoFocus="autoFocus" required="required" /> -
Email Id must be less than {CONSTANTS.NEW_EMAIL_SIZE} characters. (This is just a restriction in this tutorial)
+
Email Id must be less than {CONSTANTS.EMAIL_ID_MAX_LENGTH} characters. (This is just a restriction in this tutorial)
diff --git a/email_app/app/components/initializer.js b/email_app/app/components/initializer.js index f29c906..f7061ea 100644 --- a/email_app/app/components/initializer.js +++ b/email_app/app/components/initializer.js @@ -31,12 +31,12 @@ export default class Initializer extends Component { } checkConfiguration() { - const { client, refreshConfig } = this.props; - if (!client) { + const { app, refreshConfig } = this.props; + if (!app) { throw new Error('Application client not found.'); } - return refreshConfig(client) + return refreshConfig() .then((_) => { if (Object.keys(this.props.accounts).length > 0) { return this.context.router.push('/home'); diff --git a/email_app/app/components/mail_inbox.js b/email_app/app/components/mail_inbox.js index c97b442..34a932f 100644 --- a/email_app/app/components/mail_inbox.js +++ b/email_app/app/components/mail_inbox.js @@ -25,10 +25,11 @@ export default class MailInbox extends Component { } fetchMails() { - const { clearInbox, setMailProcessing } = this.props; - clearInbox(); - setMailProcessing(); - return this.getAppendableDataIdHandle(); + const { clearInbox, setMailProcessing, accounts } = this.props; + console.log("ACC:", accounts); +// clearInbox(); +// setMailProcessing(); +// return this.getAppendableDataIdHandle(); } refresh(e) { diff --git a/email_app/app/constants.js b/email_app/app/constants.js index 843cc1f..1036cf9 100644 --- a/email_app/app/constants.js +++ b/email_app/app/constants.js @@ -2,17 +2,14 @@ import pkg from '../package.json'; export const CONSTANTS = { LOCAL_AUTH_DATA_KEY: 'local_auth_data_key', - APPENDABLE_DATA_FILTER_TYPE: { - WHITE_LIST: 'WHITE_LIST', - BLACK_LIST: 'BLACK_LIST' - }, + INBOX_TAG_TYPE: 15003, ENCRYPTION: { PLAIN: 'PLAIN', SYMMETRIC: 'SYMMETRIC', ASYMMETRIC: 'ASYMMETRIC' }, TOTAL_INBOX_SIZE: 100, - NEW_EMAIL_SIZE: 100, + EMAIL_ID_MAX_LENGTH: 100, HOME_TABS: { INBOX: 'INBOX', OUTBOX: 'OUTBOX', @@ -30,8 +27,8 @@ export const MESSAGES = { CREATE_CORE_STRUCTURE: 'Creating Core Structure', WRITE_CONFIG_FILE: 'Creating new configuration', }, - EMAIL_ALREADY_TAKEN: 'Email already taken. Please try again', - EMAIL_TOO_LONG: 'EMAIL is too long', + EMAIL_ALREADY_TAKEN: 'Email ID already taken. Please try again', + EMAIL_ID_TOO_LONG: 'Email ID is too long', AUTHORISATION_ERROR: 'Failed to authorise', CHECK_CONFIGURATION_ERROR: 'Failed to retrieve configuration' }; diff --git a/email_app/app/containers/create_account_component.js b/email_app/app/containers/create_account_container.js similarity index 63% rename from email_app/app/containers/create_account_component.js rename to email_app/app/containers/create_account_container.js index f50173b..7b0c196 100644 --- a/email_app/app/containers/create_account_component.js +++ b/email_app/app/containers/create_account_container.js @@ -4,11 +4,6 @@ import { createAccount, createAccountError } from '../actions/create_account_act const mapStateToProps = state => { return { - token: state.initializer.token, - rootSDHandle: state.initializer.rootSDHandle, - coreData: state.initializer.coreData, - authorised: state.createAccount.authorised, - processing: state.createAccount.processing, error: state.createAccount.error }; }; @@ -16,7 +11,7 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { createAccountError: error => (dispatch(createAccountError(error))), - createAccount: () => (dispatch(createAccount())), + createAccount: emailId => (dispatch(createAccount(emailId))), }; }; diff --git a/email_app/app/containers/initializer_container.js b/email_app/app/containers/initializer_container.js index f73baa2..9597367 100644 --- a/email_app/app/containers/initializer_container.js +++ b/email_app/app/containers/initializer_container.js @@ -4,7 +4,7 @@ import { setInitializerTask, authoriseApplication, refreshConfig } from '../acti const mapStateToProps = state => { return { - client: state.initializer.client, + app: state.initializer.app, accounts: state.initializer.accounts, tasks: state.initializer.tasks, config: state.initializer.config, @@ -17,7 +17,7 @@ const mapDispatchToProps = dispatch => { setInitializerTask: task => (dispatch(setInitializerTask(task))), authoriseApplication: (appInfo, permissions, opts) => (dispatch(authoriseApplication(appInfo, permissions, opts))), - refreshConfig: mdata => (dispatch(refreshConfig(mdata))) + refreshConfig: () => (dispatch(refreshConfig())) }; }; diff --git a/email_app/app/containers/mail_inbox_container.js b/email_app/app/containers/mail_inbox_container.js index a3f636c..c2a38d5 100644 --- a/email_app/app/containers/mail_inbox_container.js +++ b/email_app/app/containers/mail_inbox_container.js @@ -1,15 +1,15 @@ import { connect } from 'react-redux'; import MailInbox from '../components/mail_inbox'; -import { setMailProcessing, clearMailProcessing } from '../actions/mail_actions'; +import { setMailProcessing, clearMailProcessing, setActiveMail, clearInbox } from '../actions/mail_actions'; const mapStateToProps = state => { return { - coreData: state.initializer.coreData, - rootSDHandle: state.initializer.rootSDHandle, - inboxSize: state.initializer.inboxSize, processing: state.mail.processing, error: state.mail.error, - token: state.initializer.token + coreData: state.initializer.coreData, + inboxSize: state.initializer.inboxSize, + app: state.initializer.app, + accounts: state.initializer.accounts }; }; @@ -17,7 +17,7 @@ const mapDispatchToProps = dispatch => { return { setMailProcessing: () => (dispatch(setMailProcessing())), clearMailProcessing: () => (dispatch(clearMailProcessing())), - pushToInbox: data => (dispatch(pushToInbox(data))), + setActiveMail: data => (dispatch(setActiveMail(data))), clearInbox: _ => (dispatch(clearInbox())), }; }; diff --git a/email_app/app/containers/mail_saved_container.js b/email_app/app/containers/mail_saved_container.js index 08b45df..facba6e 100644 --- a/email_app/app/containers/mail_saved_container.js +++ b/email_app/app/containers/mail_saved_container.js @@ -4,11 +4,11 @@ import { setMailProcessing } from '../actions/mail_actions'; const mapStateToProps = state => { return { - token: state.initializer.token, - rootSDHandle: state.initializer.rootSDHandle, coreData: state.initializer.coreData, processing: state.mail.processing, - error: state.mail.error + error: state.mail.error, + app: state.initializer.app, + accounts: state.initializer.accounts }; }; diff --git a/email_app/app/reducers/create_account.js b/email_app/app/reducers/create_account.js index 7f90ac4..2aa7636 100644 --- a/email_app/app/reducers/create_account.js +++ b/email_app/app/reducers/create_account.js @@ -1,17 +1,24 @@ import ACTION_TYPES from '../actions/actionTypes'; const initialState = { - authorised: false, processing: false, + accounts: [], error: {} }; const createAccount = (state = initialState, action) => { switch (action.type) { - case ACTION_TYPES.SET_CREATE_ACCOUNT_PROCESSING: { + case `${ACTION_TYPES.CREATE_ACCOUNT}_LOADING`: return { ...state, processing: true }; + break; + case `${ACTION_TYPES.CREATE_ACCOUNT}_SUCCESS`: { + let updatedCoreData = { ...state.coreData, id: action.payload }; + let updatedAccounts = state.accounts.push(action.payload); + return { ...state, coreData: updatedCoreData, + accounts: updatedAccounts, processing: false }; + break; } - case ACTION_TYPES.SET_CREATE_ACCOUNT_ERROR: + case ACTION_TYPES.CREATE_ACCOUNT_ERROR: return { ...state, error: action.error, processing: false }; break; default: diff --git a/email_app/app/reducers/initialiser.js b/email_app/app/reducers/initialiser.js index fc60710..d3e633c 100644 --- a/email_app/app/reducers/initialiser.js +++ b/email_app/app/reducers/initialiser.js @@ -2,9 +2,9 @@ import ACTION_TYPES from '../actions/actionTypes'; import { MESSAGES } from '../constants'; const initialState = { - client: '', + app: '', tasks: [], - accounts: null, + accounts: [], config: null, coreData: { id: '', @@ -12,8 +12,7 @@ const initialState = { saved: [], outbox: [] }, - inboxSize: 0, - rootSDHandle: 0 + inboxSize: 0 }; const initializer = (state = initialState, action) => { @@ -31,7 +30,7 @@ const initializer = (state = initialState, action) => { break; } case `${ACTION_TYPES.AUTHORISE_APP}_SUCCESS`: - return { ...state, client: action.payload }; + return { ...state, app: action.payload }; break; case `${ACTION_TYPES.GET_CONFIG}_LOADING`: { const tasks = state.tasks.slice(); @@ -42,8 +41,7 @@ const initializer = (state = initialState, action) => { case `${ACTION_TYPES.GET_CONFIG}_SUCCESS`: return { ...state, accounts: action.payload }; break; - case `${ACTION_TYPES.REFRESH_EMAIL}_SUCCESS`: - console.log("refresh email success", action, state); +/* case `${ACTION_TYPES.REFRESH_EMAIL}_SUCCESS`: return { ...state, accounts: action.payload.data }; break; case ACTION_TYPES.PUSH_TO_INBOX: { @@ -67,7 +65,7 @@ const initializer = (state = initialState, action) => { inbox: [] } }; - } + }*/ default: return state; break; diff --git a/email_app/app/reducers/mail.js b/email_app/app/reducers/mail.js index 63bf1e0..bef2630 100644 --- a/email_app/app/reducers/mail.js +++ b/email_app/app/reducers/mail.js @@ -7,18 +7,18 @@ const initialState = { const mail = (state = initialState, action) => { switch (action.type) { - case ACTION_TYPES.SET_MAIL_PROCESSING: { + case ACTION_TYPES.MAIL_PROCESSING: return { ...state, processing: true }; - } - case ACTION_TYPES.CANCEL_COMPOSE: { - return { ...state, error: Object.assign({}) }; - } - case ACTION_TYPES.CLEAR_MAIL_PROCESSING: { + break; + case ACTION_TYPES.CLEAR_MAIL_PROCESSING: return { ...state, processing: false }; - } - default: { + break; + case ACTION_TYPES.CANCEL_COMPOSE: + return { ...state, error: Object.assign({}) }; + break; + default: return state; - } + break; } }; diff --git a/email_app/app/routes.js b/email_app/app/routes.js index a506d65..6f154d2 100755 --- a/email_app/app/routes.js +++ b/email_app/app/routes.js @@ -5,7 +5,7 @@ import InitializerPage from './containers/initializer_container'; import HomePage from './containers/home_container'; import InboxPage from './containers/mail_inbox_container'; import SavedPage from './containers/mail_saved_container'; -import CreateAccountPage from './containers/create_account_component'; +import CreateAccountPage from './containers/create_account_container'; import ComposeMailPage from './containers/compose_mail_container'; let router = () => { From 28b5ce8a14493a16fa96b26f39e3f42e59433f6b Mon Sep 17 00:00:00 2001 From: Gabriel Viganotti Date: Mon, 1 May 2017 14:43:51 -0300 Subject: [PATCH 12/44] MAID-2001 Migration to the new API to use MutableData (#168) * Getting the SD handle of an existing file directly from the FILE_INDEX array instead of querying the network. * fix/packaging_issue: resolve packaging issue Resolved packaging issue to load native binaries * chore/java_email_app: refactor typo Renamed package modal to model * fix/build_config: remove unwanted build configuration Removed unwanted build configuration * fix/folder_structure: update folder structure Updated the way to store folder and files in mutable data * fix/style: resolve create service style Resolve style issue on create service page * chore/README.md: update README.md Updated README.md and removed yarn.lock file. * fix/upload: resolve upload issue Resolved uploading single file issue * chore/readme: update README.md update README.md for keytar module prerequisites. * feat/Enabling the flow for the creation of an eamil account * feat/Changing account creation to become an initializer actions * fix/Styles Reenable less-styles * feat/Adding first draft of mail inbox * feat/Creating accounts and email inbox & first draft to send email with MD * feat/Adding first draft for removing emails * feat/Draft code for email compose, send email, delete & archive, and authorise app * fix/Fixing style rendering * feat/Minor fixes & enabling authorisation flow with authenticator * feat/Encryption: encrypt emails and inbox entries with libsodium * feat/Finalising authorisation flow with authenticator * fix/packaging_config: resolve packaging issue Resolved packaging issue on OSX. * feat/Reorganising the code to keep safenet access code in a separate file * feat/Enabling archival of emails and removal of emails from archive * fix/Removing unnecessary permissions from inbox and archive MDs * fix/Removal of emails with a work around to overcome a limitation in safe_core * refactor/Minor additional reorganisation of the safenet_comm code * feat/Enabling the use of home container to store email account configuration * fix/Minor fix in the authorisation flow with authenticator * feat/Making the archive to be a private MD * docs/Design: creating a diagram detailing the internal data layout * docs/Adding the Data Model diagram to the README * feat/Enhancing errors handling * feat/Storing the auth URI in local storage and renew it when it becomes invalid * feat/Verifying email ids and reporting error when they are invalid or inexistent * docs/Updating repo URLs in README --- email_app/.compilerc | 3 + email_app/.gitignore | 1 + email_app/README.md | 6 + email_app/app/actions/actionTypes.js | 11 +- .../app/actions/create_account_actions.js | 23 +- email_app/app/actions/initializer_actions.js | 94 +- email_app/app/actions/mail_actions.js | 62 +- email_app/app/actions/nfs_actions.js | 16 - email_app/app/app.html | 7 +- email_app/app/app.js | 17 +- email_app/app/components/compose_mail.js | 83 +- email_app/app/components/create_account.js | 35 +- email_app/app/components/home.js | 24 +- email_app/app/components/initializer.js | 64 +- email_app/app/components/mail_inbox.js | 40 +- email_app/app/components/mail_list.js | 81 +- email_app/app/components/mail_saved.js | 2 +- email_app/app/constants.js | 33 +- email_app/app/containers/App.js | 11 +- .../app/containers/compose_mail_container.js | 8 +- .../containers/create_account_container.js | 7 +- email_app/app/containers/home_container.js | 4 +- .../app/containers/initializer_container.js | 11 +- .../app/containers/mail_inbox_container.js | 10 +- .../app/containers/mail_saved_container.js | 6 +- email_app/app/index.js | 41 +- email_app/app/less/main.less | 22 +- email_app/app/reducers/create_account.js | 9 +- email_app/app/reducers/initialiser.js | 79 +- email_app/app/safenet_comm.js | 256 + email_app/app/utils/app_utils.js | 51 +- email_app/design/EmailApp-DataModel.png | Bin 0 -> 83136 bytes email_app/design/EmailApp-DataModel.xml | 1 + email_app/package.json | 27 +- .../mail/controller/AppController.java | 4 +- .../mail/controller/IdCreationController.java | 2 +- .../controller/InitialisationController.java | 2 +- .../example/mail/dao/IMessagingDao.java | 4 +- .../maidsafe/example/mail/dao/IRestAPI.java | 24 +- .../example/mail/dao/LauncherAPI.java | 58 +- .../mail/{modal => model}/AppConfig.java | 2 +- .../mail/{modal => model}/AppInfo.java | 2 +- .../AppendableDataCreateRequest.java | 2 +- .../AppendableDataDataId.java | 2 +- .../AppendableDataMetadata.java | 2 +- .../mail/{modal => model}/AuthRequest.java | 2 +- .../mail/{modal => model}/AuthResponse.java | 2 +- .../mail/{modal => model}/FileResponse.java | 2 +- .../{modal => model}/HandleIdResponse.java | 2 +- .../mail/{modal => model}/Message.java | 2 +- .../mail/{modal => model}/ReaderInfo.java | 2 +- .../example/mail/{modal => model}/Result.java | 2 +- .../StructuredDataCreateRequest.java | 2 +- .../StructuredDataDataId.java | 2 +- .../StructuredDataMetadata.java | 2 +- .../StructuredDataUpdateRequest.java | 2 +- .../example/mail/scene/HomeScene.java | 2 +- .../example/mail/util/ServiceHandler.java | 2 +- markdown_editor/src/store.js | 11 +- web_hosting_manager/README.md | 69 +- .../app/components/CreateService.js | 2 +- web_hosting_manager/app/components/Home.js | 4 +- web_hosting_manager/app/lib/Downloader.js | 11 +- web_hosting_manager/app/lib/api.js | 40 +- web_hosting_manager/app/lib/tasks.js | 37 +- web_hosting_manager/app/lib/utils.js | 18 +- web_hosting_manager/app/styles/app.css | 47 +- web_hosting_manager/package.json | 2 +- web_hosting_manager/yarn.lock | 8245 ----------------- 69 files changed, 898 insertions(+), 8863 deletions(-) delete mode 100644 email_app/app/actions/nfs_actions.js create mode 100644 email_app/app/safenet_comm.js create mode 100644 email_app/design/EmailApp-DataModel.png create mode 100644 email_app/design/EmailApp-DataModel.xml rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/AppConfig.java (94%) rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/AppInfo.java (94%) rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/AppendableDataCreateRequest.java (91%) rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/AppendableDataDataId.java (90%) rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/AppendableDataMetadata.java (97%) rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/AuthRequest.java (91%) rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/AuthResponse.java (92%) rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/FileResponse.java (93%) rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/HandleIdResponse.java (86%) rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/Message.java (96%) rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/ReaderInfo.java (90%) rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/Result.java (93%) rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/StructuredDataCreateRequest.java (96%) rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/StructuredDataDataId.java (90%) rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/StructuredDataMetadata.java (95%) rename email_app_java/src/main/java/net/maidsafe/example/mail/{modal => model}/StructuredDataUpdateRequest.java (91%) delete mode 100644 web_hosting_manager/yarn.lock diff --git a/email_app/.compilerc b/email_app/.compilerc index d5cc5d8..e5d1932 100644 --- a/email_app/.compilerc +++ b/email_app/.compilerc @@ -26,5 +26,8 @@ ] } } + }, + "text/less": { + "dumpLineNumbers": "comments" } } \ No newline at end of file diff --git a/email_app/.gitignore b/email_app/.gitignore index 845ddcb..af9c934 100644 --- a/email_app/.gitignore +++ b/email_app/.gitignore @@ -8,3 +8,4 @@ Thumbs.db /dist /main.js /main.js.map +/out \ No newline at end of file diff --git a/email_app/README.md b/email_app/README.md index d99e3fa..b634bb5 100644 --- a/email_app/README.md +++ b/email_app/README.md @@ -64,3 +64,9 @@ To package apps with options: ```bash $ npm run package -- --[option] ``` + +## Application Data Model + +The following diagram depicts how the emails are stored in the SAFE network, as well as how the email app stores email accounts information. + +![Email App Data Model](./design/EmailApp-DataModel.png) diff --git a/email_app/app/actions/actionTypes.js b/email_app/app/actions/actionTypes.js index eac28c9..1272290 100644 --- a/email_app/app/actions/actionTypes.js +++ b/email_app/app/actions/actionTypes.js @@ -1,21 +1,24 @@ const ACTION_TYPES = { + // Initializer AUTHORISE_APP: 'AUTHORISE_APP', GET_CONFIG: 'GET_CONFIG', REFRESH_EMAIL: 'REFRESH_EMAIL', SET_INITIALIZER_TASK: 'SET_INITIALIZER_TASK', + STORE_NEW_ACCOUNT: 'STORE_NEW_ACCOUNT', - // legacy + // Create Account CREATE_ACCOUNT: 'CREATE_ACCOUNT', CREATE_ACCOUNT_ERROR: 'SET_CREATE_ACCOUNT_ERROR', + // Mail Inbox PUSH_MAIL: 'PUSH_MAIL', - SET_MAIL_PROCESSING: 'SET_MAIL_PROCESSING', + MAIL_PROCESSING: 'MAIL_PROCESSING', CLEAR_MAIL_PROCESSING: 'CLEAR_MAIL_PROCESSING', SET_ACTIVE_MAIL: 'SET_ACTIVE_MAIL', CANCEL_COMPOSE: 'CANCEL_COMPOSE', - CLEAR_INBOX: 'CLEAR_INBOX', - PUSH_TO_INBOX: 'PUSH_TO_INBOX' + PUSH_TO_INBOX: 'PUSH_TO_INBOX', + PUSH_TO_ARCHIVE: 'PUSH_TO_ARCHIVE' }; export default ACTION_TYPES; diff --git a/email_app/app/actions/create_account_actions.js b/email_app/app/actions/create_account_actions.js index 8569023..cb920ea 100644 --- a/email_app/app/actions/create_account_actions.js +++ b/email_app/app/actions/create_account_actions.js @@ -1,28 +1,13 @@ -import { CONSTANTS } from '../constants'; import ACTION_TYPES from './actionTypes'; - -var accountResolver; -var accountRejecter; -const accountPromise = new Promise((resolve, reject) => { - accountResolver = resolve; - accountRejecter = reject; -}); +import { setupAccount } from '../safenet_comm'; export const createAccount = (emailId) => { return function (dispatch, getState) { - dispatch({ + let app = getState().initializer.app; + return dispatch({ type: ACTION_TYPES.CREATE_ACCOUNT, - payload: accountPromise + payload: setupAccount(app, emailId) }); - - // FIXME: store private key for encryption in app's container mapped to emailId - - let app = getState().initializer.app; - return app.mutableData.newRandomPublic(CONSTANTS.INBOX_TAG_TYPE) - .then((md) => md.quickSetup({})) - // FIXME: map this address to emailId in publicNames - .then((md) => accountResolver(md)) - .catch(accountRejecter); }; }; diff --git a/email_app/app/actions/initializer_actions.js b/email_app/app/actions/initializer_actions.js index a2da244..48dc2ef 100644 --- a/email_app/app/actions/initializer_actions.js +++ b/email_app/app/actions/initializer_actions.js @@ -1,65 +1,81 @@ -import { initializeApp, fromAuthURI } from 'safe-app'; - import ACTION_TYPES from './actionTypes'; - -var authResolver; -var authRejecter; -const authPromise = () => new Promise((resolve, reject) => { - authResolver = resolve; - authRejecter = reject; -}); +import { authApp, connect, readConfig, writeConfig, + readInboxEmails, readArchivedEmails } from '../safenet_comm'; export const setInitializerTask = (task) => ({ type: ACTION_TYPES.SET_INITIALIZER_TASK, task }); -export const receiveResponse = (uri) => { +export const onAuthFailure = (err) => { return { type: ACTION_TYPES.AUTHORISE_APP, - payload: fromAuthURI(uri) - .then((app) => authResolver ? authResolver(app) : app) - } + payload: Promise.reject(err) + }; }; -export const authoriseApplication = (appInfo, permissions, opts) => { - +export const receiveResponse = (uri) => { return function (dispatch) { - dispatch({ + return dispatch({ type: ACTION_TYPES.AUTHORISE_APP, - payload: authPromise() + payload: connect(uri) }); + }; +}; - return initializeApp(appInfo) - .then((app) => - process.env.SAFE_FAKE_AUTH - ? app.auth.loginForTest(permissions, opts) - .then(app => authResolver(app)) - : app.auth.genAuthUri(permissions, opts) - .then(resp => app.auth.openUri(resp.uri)) - ).catch(authRejecter); - +export const authoriseApplication = () => { + return function (dispatch) { + return dispatch({ + type: ACTION_TYPES.AUTHORISE_APP, + payload: new Promise((resolve, reject) => { + authApp() + .then(resolve) + .catch(reject); + }) + }) + .catch(_ => {}); }; }; export const refreshConfig = () => { - return function (dispatch, getState) { - dispatch({ + let app = getState().initializer.app; + return dispatch({ type: ACTION_TYPES.GET_CONFIG, - payload: authPromise() + payload: readConfig(app) + }); + }; +}; + +export const storeNewAccount = (account) => { + return function (dispatch, getState) { + let app = getState().initializer.app; + return dispatch({ + type: ACTION_TYPES.STORE_NEW_ACCOUNT, + payload: writeConfig(app, account) }); + }; +}; - let accounts = {}; +export const refreshEmail = (account) => { + return function (dispatch, getState) { let app = getState().initializer.app; - return app.auth.refreshContainerAccess() - .then(() => app.auth.getHomeContainer()) - .then((mdata) => mdata.getEntries() - .then((entries) => entries.forEach((name, valV) => { - accounts[name.toString()] = valV.buf.toString(); - }) - .then(() => authResolver(accounts)) - ) - ).catch(authRejecter); + return dispatch({ + type: ACTION_TYPES.REFRESH_EMAIL, + payload: readInboxEmails(app, account, + (inboxEntry) => { + dispatch({ + type: ACTION_TYPES.PUSH_TO_INBOX, + payload: inboxEntry + }); + }) + .then(() => readArchivedEmails(app, account, + (archiveEntry) => { + dispatch({ + type: ACTION_TYPES.PUSH_TO_ARCHIVE, + payload: archiveEntry + }); + })) + }); }; }; diff --git a/email_app/app/actions/mail_actions.js b/email_app/app/actions/mail_actions.js index e992c3e..3ae1873 100644 --- a/email_app/app/actions/mail_actions.js +++ b/email_app/app/actions/mail_actions.js @@ -1,22 +1,58 @@ import ACTION_TYPES from './actionTypes'; +import { storeEmail, removeInboxEmail, removeArchivedEmail, archiveEmail } from '../safenet_comm'; -export const setMailProcessing = () => ({ - type: ACTION_TYPES.SET_MAIL_PROCESSING -}); +export const sendEmail = (email, to) => { + return function (dispatch, getState) { + let app = getState().initializer.app; + return dispatch({ + type: ACTION_TYPES.MAIL_PROCESSING, + payload: storeEmail(app, email, to) + .then(() => dispatch(clearMailProcessing)) + .then(() => Promise.resolve()) + }); + }; +}; -export const clearMailProcessing= _ => ({ - type: ACTION_TYPES.CLEAR_MAIL_PROCESSING -}); +export const saveEmail = (account, key) => { + return function (dispatch, getState) { + let app = getState().initializer.app; + return dispatch({ + type: ACTION_TYPES.MAIL_PROCESSING, + payload: archiveEmail(app, account, key) + .then(() => dispatch(clearMailProcessing)) + .then(() => Promise.resolve()) + }); + }; +}; -export const setActiveMail = (data) => ({ - type: ACTION_TYPES.SET_ACTIVE_MAIL, - data -}); +export const deleteInboxEmail = (account, key) => { + return function (dispatch, getState) { + let app = getState().initializer.app; + return dispatch({ + type: ACTION_TYPES.MAIL_PROCESSING, + payload: removeInboxEmail(app, account, key) + .then(() => dispatch(clearMailProcessing)) + .then(() => Promise.resolve()) + }); + }; +}; + +export const deleteSavedEmail = (account, key) => { + return function (dispatch, getState) { + let app = getState().initializer.app; + return dispatch({ + type: ACTION_TYPES.MAIL_PROCESSING, + payload: removeArchivedEmail(app, account, key) + .then(() => dispatch(clearMailProcessing)) + .then(() => Promise.resolve()) + }); + }; +}; -export const clearInbox = () => ({ - type: ACTION_TYPES.SET_ACTIVE_MAIL +export const clearMailProcessing = _ => ({ + type: ACTION_TYPES.CLEAR_MAIL_PROCESSING }); -export const cancelCompose = () => ({ +export const cancelCompose = _ => ({ type: ACTION_TYPES.CANCEL_COMPOSE }); diff --git a/email_app/app/actions/nfs_actions.js b/email_app/app/actions/nfs_actions.js deleted file mode 100644 index 71e7500..0000000 --- a/email_app/app/actions/nfs_actions.js +++ /dev/null @@ -1,16 +0,0 @@ -import ACTION_TYPES from './actionTypes'; -import { CONSTANTS } from '../constants'; - -export const writeConfigFile = (coreId) => { - // FIXME: are these even needed? - return { - type: ACTION_TYPES.WRITE_CONFIG_FILE, - coreId - } -}; - -export const getConfigFile = () => { - return { - type: ACTION_TYPES.GET_CONFIG_FILE - }; -}; diff --git a/email_app/app/app.html b/email_app/app/app.html index 899999d..801f634 100755 --- a/email_app/app/app.html +++ b/email_app/app/app.html @@ -8,9 +8,10 @@ + -
+
- \ No newline at end of file + diff --git a/email_app/app/app.js b/email_app/app/app.js index 2659747..6ee9b8d 100644 --- a/email_app/app/app.js +++ b/email_app/app/app.js @@ -6,23 +6,18 @@ import { syncHistoryWithStore } from 'react-router-redux'; import { ipcRenderer as ipc } from 'electron'; import routes from './routes'; import configureStore from './store/configureStore'; -import { receiveResponse } from "./actions/initializer_actions"; +import { receiveResponse, onAuthFailure } from "./actions/initializer_actions"; const store = configureStore(); const history = syncHistoryWithStore(hashHistory, store); - -const listenForAuthReponse = (event, response) => { - // TODO parse response - if (response) { +ipc.on('auth-response', (event, response) => { + if (response && response.indexOf('safe-') == 0) { store.dispatch(receiveResponse(response)); // TODO do it concurrently (no to linked dispatch) } else { - // store.dispatch(onAuthFailure(new Error('Authorisation failed'))); + store.dispatch(onAuthFailure(new Error('Authorisation failed'))); } -}; - -ipc.on('auth-response', listenForAuthReponse); - +}); export default class App extends React.Component { render() { @@ -30,4 +25,4 @@ export default class App extends React.Component { ); } -} \ No newline at end of file +} diff --git a/email_app/app/components/compose_mail.js b/email_app/app/components/compose_mail.js index 20d5d9e..b712efd 100644 --- a/email_app/app/components/compose_mail.js +++ b/email_app/app/components/compose_mail.js @@ -1,78 +1,19 @@ -import React, { Component, PropTypes } from 'react'; -import * as base64 from 'urlsafe-base64'; -import { hashEmailId, showError, showSuccess } from '../utils/app_utils'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { showError, showSuccess } from '../utils/app_utils'; import { CONSTANTS } from '../constants'; export default class ComposeMail extends Component { - static contextTypes = { - router: PropTypes.object.isRequired - }; - constructor() { super(); - this.newMail = {}; this.tempMailContent = null; - this.newMailId = null; - this.appendableDataId = null; this.sendMail = this.sendMail.bind(this); - this.createMail = this.createMail.bind(this); this.handleTextLimit = this.handleTextLimit.bind(this); + this.handleCancel = this.handleCancel.bind(this); } - - - createMail(cipherHandleId) { - const { token, createImmutableDataWriterHandle, writeImmutableData, putImmutableData, closeImmutableDataWriter, clearMailProcessing } = this.props; - - const dropWriter = (writerHandle) => { - closeImmutableDataWriter(token, writerHandle) - .then(res => { - if (res.error) { - return console.error('Drop Immutable Data Writer Handle Error', res.error.message); - } - console.warn('Immutable Data Writer Handle Dropped'); - }); - }; - - const save = (writerHandleId) => { - putImmutableData(token, writerHandleId, cipherHandleId) - .then(res => { - if (res.error) { - clearMailProcessing(); - return showError('Create Immutable Data Writer Error', res.error.message); - } - dropWriter(writerHandleId); - return this.appendAppendableData(res.payload.data.handleId); - }); - }; - - const write = (writerHandleId) => { - writeImmutableData(token, writerHandleId, this.newMail) - .then(res => { - if (res.error) { - clearMailProcessing(); - return showError('Create Immutable Data Writer Error', res.error.message); - } - return save(writerHandleId); - }) - }; - - const createImmutWriter = () => { - createImmutableDataWriterHandle(token) - .then(res => { - if (res.error) { - clearMailProcessing(); - return showError('Create Immutable Data Writer Error', res.error.message); - } - return write(res.payload.data.handleId); - }); - }; - createImmutWriter(); - } - - sendMail(e) { - const { token, fromMail, setMailProcessing } = this.props; + const { app, fromMail, sendEmail } = this.props; e.preventDefault(); const mailTo = this.mailTo.value.trim(); @@ -84,14 +25,16 @@ export default class ComposeMail extends Component { if (mailContent.length > CONSTANTS.MAIL_CONTENT_LIMIT) { return showError('Mail Content is too Long', 'Mail Content is too long!'); } - this.newMail = { + + let newEmail = { subject: mailSub, from: fromMail, time: (new Date()).toUTCString(), body: mailContent }; - setMailProcessing(); - return this.getAppendableDataIdHandle(mailTo); + return sendEmail(newEmail, mailTo) + .then(() => this.context.router.push('/home')) + .catch((err) => showError('Error sending email', err)); } handleCancel() { @@ -129,7 +72,7 @@ export default class ComposeMail extends Component {
+ }} required="required" defaultValue=" " />
Only { CONSTANTS.MAIL_CONTENT_LIMIT } characters allowed. (This is just a restriction in this tutorial to not handle multiple chunks for content)
@@ -147,3 +90,7 @@ export default class ComposeMail extends Component { ); } } + +ComposeMail.contextTypes = { + router: PropTypes.object.isRequired +}; diff --git a/email_app/app/components/create_account.js b/email_app/app/components/create_account.js index 16dcbf8..0ae6e05 100644 --- a/email_app/app/components/create_account.js +++ b/email_app/app/components/create_account.js @@ -1,19 +1,20 @@ -import React, { Component, PropTypes } from 'react'; -import { hashEmailId } from '../utils/app_utils'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import { MESSAGES, CONSTANTS } from '../constants'; export default class CreateAccount extends Component { - static propTypes = { - }; - - static contextTypes = { - router: PropTypes.object.isRequired - }; - constructor() { super(); this.errMrg = null; this.handleCreateAccount = this.handleCreateAccount.bind(this); + this.storeCreatedAccount = this.storeCreatedAccount.bind(this); + } + + storeCreatedAccount() { + const { newAccount, storeNewAccount, createAccountError } = this.props; + return storeNewAccount(newAccount) + .then((_) => this.context.router.push('/home')) + .catch((e) => createAccountError(new Error(e))); } handleCreateAccount(e) { @@ -23,12 +24,20 @@ export default class CreateAccount extends Component { if (!emailId.trim()) { return; } + if (emailId.length > CONSTANTS.EMAIL_ID_MAX_LENGTH) { return createAccountError(new Error(MESSAGES.EMAIL_ID_TOO_LONG)); } + return createAccount(emailId) - .then(() => this.context.router.push('/home')); - } + .then(this.storeCreatedAccount) + .catch((err) => { + if (err.name === 'ERR_DATA_EXISTS') { + return createAccountError(new Error(MESSAGES.EMAIL_ALREADY_TAKEN)); + } + return createAccountError(err); + }); + }; render() { const { processing, error } = this.props; @@ -55,3 +64,7 @@ export default class CreateAccount extends Component { ); } } + +CreateAccount.contextTypes = { + router: PropTypes.object.isRequired +}; diff --git a/email_app/app/components/home.js b/email_app/app/components/home.js index 3bdcfa1..25da6ad 100755 --- a/email_app/app/components/home.js +++ b/email_app/app/components/home.js @@ -1,23 +1,13 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import { Link, IndexLink } from 'react-router'; import className from 'classnames'; import pkg from '../../package.json'; export default class Home extends Component { - static contextTypes = { - router: PropTypes.object.isRequired - }; - - static propTypes = { - - }; - render() { const { router } = this.context; - const { coreData } = this.props; - const inboxLength = coreData.inbox.length; - const savedLength = coreData.saved.length; - const outboxLength = coreData.outbox.length; + const { coreData, inboxSize, savedSize } = this.props; return (
@@ -40,7 +30,7 @@ export default class Home extends Component { email Inbox - {inboxLength === 0 ? '' : `${inboxLength}`} + {inboxSize === 0 ? '' : `${inboxSize}`}
@@ -49,7 +39,7 @@ export default class Home extends Component { drafts Saved - {savedLength === 0 ? '' : `${savedLength}`} + {savedSize === 0 ? '' : `${savedSize}`}
@@ -62,3 +52,7 @@ export default class Home extends Component { ); } } + +Home.contextTypes = { + router: PropTypes.object.isRequired +}; diff --git a/email_app/app/components/initializer.js b/email_app/app/components/initializer.js index f7061ea..7790387 100644 --- a/email_app/app/components/initializer.js +++ b/email_app/app/components/initializer.js @@ -1,55 +1,51 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import { remote } from 'electron'; -import { CONSTANTS, AUTH_PAYLOAD, MESSAGES } from '../constants'; +import { showError } from '../utils/app_utils'; +import { MESSAGES, APP_STATUS } from '../constants'; -const showDialog = (title, message) => { - remote.dialog.showMessageBox({ - type: 'error', - buttons: ['Ok'], - title, - message - }, _ => { remote.getCurrentWindow().close(); }); -}; +const showAuthError = _ => showError('Authorisation failed', + MESSAGES.AUTHORISATION_ERROR, + _ => { remote.getCurrentWindow().close(); }); export default class Initializer extends Component { - static contextTypes = { - router: PropTypes.object.isRequired - }; - constructor() { super(); - this.checkConfiguration = this.checkConfiguration.bind(this); + this.refreshConfig = this.refreshConfig.bind(this); } componentDidMount() { - this.props.authoriseApplication(AUTH_PAYLOAD, {"_publicNames" : ["Insert"]}) - .then((_) => this.checkConfiguration()) - .catch((err) => { - console.error(err) - return showDialog('Authorisation Error', MESSAGES.AUTHORISATION_ERROR); - }); - } + const { setInitializerTask, authoriseApplication } = this.props; + setInitializerTask(MESSAGES.INITIALIZE.AUTHORISE_APP); - checkConfiguration() { - const { app, refreshConfig } = this.props; - if (!app) { - throw new Error('Application client not found.'); - } + return authoriseApplication(); + } + refreshConfig() { + const { setInitializerTask, refreshConfig } = this.props; + setInitializerTask(MESSAGES.INITIALIZE.CHECK_CONFIGURATION); return refreshConfig() .then((_) => { if (Object.keys(this.props.accounts).length > 0) { return this.context.router.push('/home'); - } else { - return this.context.router.push('/create_account'); } + showAuthError(); }) - .catch((err) => { - console.error(err) - return showDialog('Error fetching configuration', MESSAGES.CHECK_CONFIGURATION_ERROR); + .catch((_) => { + console.log("No email account found"); + return this.context.router.push('/create_account'); }); } + componentDidUpdate(prevProps, prevState) { + const { app_status, app } = this.props; + if (prevProps.app_status === APP_STATUS.AUTHORISING + && app_status === APP_STATUS.AUTHORISATION_FAILED) { + showAuthError(); + } else if (app && app_status === APP_STATUS.AUTHORISED) { + return this.refreshConfig(); + } + } render() { const { tasks } = this.props; @@ -70,3 +66,7 @@ export default class Initializer extends Component { ); } } + +Initializer.contextTypes = { + router: PropTypes.object.isRequired +}; diff --git a/email_app/app/components/mail_inbox.js b/email_app/app/components/mail_inbox.js index 34a932f..f9e1f59 100644 --- a/email_app/app/components/mail_inbox.js +++ b/email_app/app/components/mail_inbox.js @@ -1,44 +1,32 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; import MailList from './mail_list'; -import * as base64 from 'urlsafe-base64'; -import { showError, hashEmailId } from '../utils/app_utils'; +import { showError } from '../utils/app_utils'; import { CONSTANTS } from '../constants'; export default class MailInbox extends Component { constructor() { super(); - this.appendableDataHandle = 0; - this.dataLength = 0; - this.currentIndex = 0; - this.refresh = this.refresh.bind(this); this.fetchMails = this.fetchMails.bind(this); - this.fetchMail = this.fetchMail.bind(this); - this.refresh = this.refresh.bind(this); } componentDidMount() { this.fetchMails(); } - fetchMail(id) { - - } - - fetchMails() { - const { clearInbox, setMailProcessing, accounts } = this.props; - console.log("ACC:", accounts); -// clearInbox(); -// setMailProcessing(); -// return this.getAppendableDataIdHandle(); - } - - refresh(e) { + fetchMails(e) { if (e) { e.preventDefault(); } - this.currentIndex = 0; - this.dataLength = 0; - this.fetchMails(); + + const { refreshEmail, accounts } = this.props; + // TODO: Eventually the app can allow to choose which email account, + // it now supports only one. + let chosenAccount = accounts; + refreshEmail(chosenAccount) + .catch((error) => { + console.error('Failed fetching emails: ', error); + showError('Failed fetching emails: ', error); + }); } render() { @@ -49,7 +37,7 @@ export default class MailInbox extends Component { Inbox Space Used: {this.props.inboxSize}KB of {CONSTANTS.TOTAL_INBOX_SIZE}KB
-
diff --git a/email_app/app/components/mail_list.js b/email_app/app/components/mail_list.js index 5dfb5ae..9b78ae1 100644 --- a/email_app/app/components/mail_list.js +++ b/email_app/app/components/mail_list.js @@ -1,23 +1,18 @@ -import React, { Component, PropTypes } from 'react'; -import { remote } from 'electron'; -import * as base64 from 'urlsafe-base64'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import dateformat from 'dateformat'; -import { showError, showSuccess, hashEmailId } from '../utils/app_utils'; +import { showError } from '../utils/app_utils'; import { CONSTANTS } from '../constants'; export default class MailList extends Component { - static contextTypes = { - router: PropTypes.object.isRequired - }; - constructor() { super(); - this.appendableHandlerId = null; this.listColors = {}; - this.activeIndex = null; this.activeType = null; this.goBack = this.goBack.bind(this); - this.handleDelete = this.handleDelete.bind(this); + this.refreshEmail = this.refreshEmail.bind(this); + this.handleDeleteFromInbox = this.handleDeleteFromInbox.bind(this); + this.handleDeleteSaved = this.handleDeleteSaved.bind(this); this.handleSave = this.handleSave.bind(this); } @@ -26,7 +21,7 @@ export default class MailList extends Component { switch (this.activeType) { case CONSTANTS.HOME_TABS.INBOX: { - return this.props.inbox.refresh(); + return this.props.refreshEmail(); } case CONSTANTS.HOME_TABS.SAVED: { router.push('/home'); @@ -35,35 +30,72 @@ export default class MailList extends Component { } } - handleDelete(e) { + refreshEmail(account) { + this.props.refreshEmail(account) + .catch((error) => { + console.error('Fetching emails failed: ', error); + showError('Fetching emails failed: ', error); + }); + } + + handleDeleteFromInbox(e) { e.preventDefault(); + const { accounts, deleteInboxEmail, refreshEmail } = this.props; + deleteInboxEmail(accounts, e.target.dataset.index) + .catch((error) => { + console.error('Failed trying to delete email from inbox: ', error); + showError('Failed trying to delete email from inbox: ', error); + }) + .then(() => this.refreshEmail(accounts)) + } + + handleDeleteSaved(e) { + e.preventDefault(); + const { accounts, deleteSavedEmail, refreshEmail } = this.props; + deleteSavedEmail(accounts, e.target.dataset.index) + .catch((error) => { + console.error('Failed trying to delete saved email: ', error); + showError('Failed trying to delete saved email: ', error); + }) + .then(() => this.refreshEmail(accounts)) } handleSave(e) { e.preventDefault(); + const { accounts, saveEmail, refreshEmail } = this.props; + // TODO: Eventually the app can allow to choose which email account, + // it now supports only one. + let chosenAccount = accounts; + saveEmail(chosenAccount, e.target.dataset.index) + .catch((error) => { + console.error('Failed trying to save the email: ', error); + showError('Failed trying to save the email: ', error); + }) + .then(() => this.refreshEmail(chosenAccount)) } render() { const self = this; - const { processing, coreData, error, inbox, outbox, saved } = this.props; + const { processing, coreData, error, inboxSize, inbox, savedSize, saved } = this.props; let container = null; if (processing) { container =
  • Loading...
  • } else if (Object.keys(error).length > 0) { - container =
  • Error in fetching mails!
  • + container =
  • Error in fetching emails!
  • } else { if (inbox) { this.activeType = CONSTANTS.HOME_TABS.INBOX; container = (
    { - coreData.inbox.length === 0 ?
  • Inbox empty
  • : coreData.inbox.map((mail, i) => { + inboxSize === 0 ?
  • Inbox empty
  • : Object.keys(coreData.inbox).map((key) => { + let mail = coreData.inbox[key]; if (!self.listColors.hasOwnProperty(mail.from)) { self.listColors[mail.from] = `bg-color-${Object.keys(self.listColors).length % 10}` } return ( -
  • +
  • {mail.from[0]}
    @@ -75,10 +107,10 @@ export default class MailList extends Component {
  • - +
    - +
    @@ -93,7 +125,8 @@ export default class MailList extends Component { container = (
    { - coreData.saved.length === 0 ?
  • Saved empty
  • : coreData.saved.map((mail, i) => { + savedSize === 0 ?
  • Saved empty
  • : Object.keys(coreData.saved).map((key) => { + let mail = coreData.saved[key]; if (!mail) { return; } @@ -101,7 +134,7 @@ export default class MailList extends Component { self.listColors[mail.from] = `bg-color-${Object.keys(self.listColors).length % 10}` } return ( -
  • +
  • {mail.from[0]}
    @@ -113,7 +146,7 @@ export default class MailList extends Component {
  • - +
    @@ -131,3 +164,7 @@ export default class MailList extends Component { ); } } + +MailList.contextTypes = { + router: PropTypes.object.isRequired +}; diff --git a/email_app/app/components/mail_saved.js b/email_app/app/components/mail_saved.js index 9994065..edf2b31 100644 --- a/email_app/app/components/mail_saved.js +++ b/email_app/app/components/mail_saved.js @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; import MailList from './mail_list'; export default class MailSaved extends Component { diff --git a/email_app/app/constants.js b/email_app/app/constants.js index 1036cf9..cef0ddc 100644 --- a/email_app/app/constants.js +++ b/email_app/app/constants.js @@ -1,24 +1,32 @@ -import pkg from '../package.json'; - export const CONSTANTS = { LOCAL_AUTH_DATA_KEY: 'local_auth_data_key', - INBOX_TAG_TYPE: 15003, - ENCRYPTION: { - PLAIN: 'PLAIN', - SYMMETRIC: 'SYMMETRIC', - ASYMMETRIC: 'ASYMMETRIC' - }, + TAG_TYPE_DNS: 15001, + TAG_TYPE_INBOX: 15003, + TAG_TYPE_EMAIL_ARCHIVE: 15004, + SERVICE_NAME_POSTFIX: "@email", + MD_KEY_EMAIL_INBOX: "email_inbox", + MD_KEY_EMAIL_ARCHIVE: "email_archive", + MD_KEY_EMAIL_ID: "email_id", + MD_KEY_EMAIL_ENC_SECRET_KEY: "__email_enc_sk", + MD_KEY_EMAIL_ENC_PUBLIC_KEY: "__email_enc_pk", TOTAL_INBOX_SIZE: 100, EMAIL_ID_MAX_LENGTH: 100, HOME_TABS: { INBOX: 'INBOX', - OUTBOX: 'OUTBOX', SAVED: 'SAVED' }, MAIL_CONTENT_LIMIT: 150, DATE_FORMAT: 'h:MM-mmm dd' }; +export const APP_STATUS = { + AUTHORISING: 'AUTHORISING', + AUTHORISATION_FAILED: 'AUTHORISATION_FAILED', + AUTHORISED: 'AUTHORISED', + READING_CONFIG: 'READING_CONFIG', + READY: 'READY' +} + export const MESSAGES = { INITIALIZE: { AUTHORISE_APP: 'Authorising Application', @@ -29,12 +37,7 @@ export const MESSAGES = { }, EMAIL_ALREADY_TAKEN: 'Email ID already taken. Please try again', EMAIL_ID_TOO_LONG: 'Email ID is too long', + EMAIL_ID_NOT_FOUND: 'Email ID not found on the network', AUTHORISATION_ERROR: 'Failed to authorise', CHECK_CONFIGURATION_ERROR: 'Failed to retrieve configuration' }; - -export const AUTH_PAYLOAD = { - id: pkg.identifier, - name: pkg.productName, - vendor: pkg.vendor -}; diff --git a/email_app/app/containers/App.js b/email_app/app/containers/App.js index 93a4ebc..b9f5cf5 100755 --- a/email_app/app/containers/App.js +++ b/email_app/app/containers/App.js @@ -1,10 +1,7 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; export default class App extends Component { - static propTypes = { - children: PropTypes.element.isRequired - }; - render() { return (
    @@ -13,3 +10,7 @@ export default class App extends Component { ); } } + +App.propTypes = { + children: PropTypes.element.isRequired +}; diff --git a/email_app/app/containers/compose_mail_container.js b/email_app/app/containers/compose_mail_container.js index 0c28f2f..11ae136 100644 --- a/email_app/app/containers/compose_mail_container.js +++ b/email_app/app/containers/compose_mail_container.js @@ -1,10 +1,10 @@ import { connect } from 'react-redux'; import ComposeMail from '../components/compose_mail'; -import { cancelCompose, setMailProcessing, clearMailProcessing } from '../actions/mail_actions'; +import { cancelCompose, sendEmail, clearMailProcessing } from '../actions/mail_actions'; const mapStateToProps = state => { return { - token: state.initializer.token, + app: state.initializer.app, fromMail: state.initializer.coreData.id, error: state.mail.error, processing: state.mail.processing @@ -13,9 +13,9 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - setMailProcessing: _ => (dispatch(setMailProcessing())), + sendEmail: (email, to) => (dispatch(sendEmail(email, to))), clearMailProcessing: _ => (dispatch(clearMailProcessing())), - cancelCompose: _ => dispatch(cancelCompose()), + cancelCompose: _ => dispatch(cancelCompose()) }; }; diff --git a/email_app/app/containers/create_account_container.js b/email_app/app/containers/create_account_container.js index 7b0c196..3012740 100644 --- a/email_app/app/containers/create_account_container.js +++ b/email_app/app/containers/create_account_container.js @@ -1,10 +1,14 @@ import { connect } from 'react-redux'; import CreateAccount from '../components/create_account'; import { createAccount, createAccountError } from '../actions/create_account_actions'; +import { storeNewAccount } from '../actions/initializer_actions'; const mapStateToProps = state => { return { - error: state.createAccount.error + error: state.createAccount.error, + processing: state.createAccount.processing, + newAccount: state.createAccount.newAccount, + coreData: state.initializer.coreData }; }; @@ -12,6 +16,7 @@ const mapDispatchToProps = dispatch => { return { createAccountError: error => (dispatch(createAccountError(error))), createAccount: emailId => (dispatch(createAccount(emailId))), + storeNewAccount: account => (dispatch(storeNewAccount(account))) }; }; diff --git a/email_app/app/containers/home_container.js b/email_app/app/containers/home_container.js index b03aa28..190c4a0 100644 --- a/email_app/app/containers/home_container.js +++ b/email_app/app/containers/home_container.js @@ -3,7 +3,9 @@ import Home from '../components/home'; const mapStateToProps = state => { return { - coreData: state.initializer.coreData + coreData: state.initializer.coreData, + inboxSize: state.initializer.inboxSize, + savedSize: state.initializer.savedSize }; }; diff --git a/email_app/app/containers/initializer_container.js b/email_app/app/containers/initializer_container.js index 9597367..7e7571c 100644 --- a/email_app/app/containers/initializer_container.js +++ b/email_app/app/containers/initializer_container.js @@ -1,13 +1,14 @@ import { connect } from 'react-redux'; import Initializer from '../components/initializer'; -import { setInitializerTask, authoriseApplication, refreshConfig } from '../actions/initializer_actions'; +import { setInitializerTask, authoriseApplication, + refreshConfig, refreshEmail } from '../actions/initializer_actions'; const mapStateToProps = state => { return { + app_status: state.initializer.app_status, app: state.initializer.app, accounts: state.initializer.accounts, tasks: state.initializer.tasks, - config: state.initializer.config, coreData: state.initializer.coreData }; }; @@ -15,9 +16,9 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { setInitializerTask: task => (dispatch(setInitializerTask(task))), - authoriseApplication: (appInfo, permissions, opts) => - (dispatch(authoriseApplication(appInfo, permissions, opts))), - refreshConfig: () => (dispatch(refreshConfig())) + authoriseApplication: () => (dispatch(authoriseApplication())), + refreshConfig: () => (dispatch(refreshConfig())), + refreshEmail: (account) => (dispatch(refreshEmail(account))) }; }; diff --git a/email_app/app/containers/mail_inbox_container.js b/email_app/app/containers/mail_inbox_container.js index c2a38d5..0eed22c 100644 --- a/email_app/app/containers/mail_inbox_container.js +++ b/email_app/app/containers/mail_inbox_container.js @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; import MailInbox from '../components/mail_inbox'; -import { setMailProcessing, clearMailProcessing, setActiveMail, clearInbox } from '../actions/mail_actions'; +import { refreshInbox, clearMailProcessing, deleteInboxEmail, saveEmail } from '../actions/mail_actions'; +import { refreshEmail } from '../actions/initializer_actions'; const mapStateToProps = state => { return { @@ -8,6 +9,7 @@ const mapStateToProps = state => { error: state.mail.error, coreData: state.initializer.coreData, inboxSize: state.initializer.inboxSize, + savedSize: state.initializer.savedSize, app: state.initializer.app, accounts: state.initializer.accounts }; @@ -15,10 +17,10 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - setMailProcessing: () => (dispatch(setMailProcessing())), + refreshEmail: (account) => (dispatch(refreshEmail(account))), + deleteInboxEmail: (account, key) => (dispatch(deleteInboxEmail(account, key))), clearMailProcessing: () => (dispatch(clearMailProcessing())), - setActiveMail: data => (dispatch(setActiveMail(data))), - clearInbox: _ => (dispatch(clearInbox())), + saveEmail: (account, key) => (dispatch(saveEmail(account, key))) }; }; export default connect(mapStateToProps, mapDispatchToProps)(MailInbox); diff --git a/email_app/app/containers/mail_saved_container.js b/email_app/app/containers/mail_saved_container.js index facba6e..a45282a 100644 --- a/email_app/app/containers/mail_saved_container.js +++ b/email_app/app/containers/mail_saved_container.js @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; import MailSaved from '../components/mail_saved'; -import { setMailProcessing } from '../actions/mail_actions'; +import { deleteSavedEmail } from '../actions/mail_actions'; +import { refreshEmail } from '../actions/initializer_actions'; const mapStateToProps = state => { return { @@ -14,7 +15,8 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - setMailProcessing: () => (dispatch(setMailProcessing())), + refreshEmail: (account) => (dispatch(refreshEmail(account))), + deleteSavedEmail: (account, key) => (dispatch(deleteSavedEmail(account, key))) }; }; diff --git a/email_app/app/index.js b/email_app/app/index.js index 75cef93..7378206 100755 --- a/email_app/app/index.js +++ b/email_app/app/index.js @@ -1,23 +1,14 @@ import { app, BrowserWindow } from 'electron'; -import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer'; -import { enableLiveReload } from 'electron-compile'; -require("babel-polyfill"); - // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. let mainWindow; - -const sendResponse = (success) => { - mainWindow.webContents.send('auth-response', success ? success : ''); +const sendResponse = (res) => { + mainWindow.webContents.send('auth-response', res ? res : ''); }; -const isDevMode = process.execPath.match(/[\\/]electron/); - -// if (isDevMode) enableLiveReload({strategy: 'react-hmr'}); - -const createWindow = async () => { +const createWindow = () => { // Create the browser window. mainWindow = new BrowserWindow({ width: 800, @@ -28,10 +19,7 @@ const createWindow = async () => { mainWindow.loadURL(`file://${__dirname}/app.html`); // Open the DevTools. - if (isDevMode) { - await installExtension(REACT_DEVELOPER_TOOLS); - mainWindow.webContents.openDevTools(); - } + mainWindow.webContents.openDevTools(); // Emitted when the window is closed. mainWindow.on('closed', () => { @@ -40,15 +28,6 @@ const createWindow = async () => { // when you should delete the corresponding element. mainWindow = null; }); -}; - -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -// Some APIs can only be used after this event occurs. - - -app.on('ready', async () => { - await createWindow(); const shouldQuit = app.makeSingleInstance(function(commandLine) { if (commandLine.length >= 2 && commandLine[1]) { @@ -65,8 +44,13 @@ app.on('ready', async () => { if (shouldQuit) { app.quit(); } -}); +}; + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.on('ready', createWindow); // Quit when all windows are closed. app.on('window-all-closed', () => { @@ -85,9 +69,6 @@ app.on('activate', () => { } }); -// In this file you can include the rest of your app's specific main process -// code. You can also put them in separate files and import them here. - app.on('open-url', function (e, url) { sendResponse(url); -}); \ No newline at end of file +}); diff --git a/email_app/app/less/main.less b/email_app/app/less/main.less index 781eb34..6e0a5b7 100644 --- a/email_app/app/less/main.less +++ b/email_app/app/less/main.less @@ -1,11 +1,11 @@ -@import "material_icons"; -@import "variables"; -@import "text_components"; -@import "form"; -@import "buttons"; -@import "base"; -@import "authenticate"; -@import "home"; -@import "list"; -@import "compose_mail"; -@import "view_mail"; +@import "material_icons.less"; +@import "variables.less"; +@import "text_components.less"; +@import "form.less"; +@import "buttons.less"; +@import "base.less"; +@import "authenticate.less"; +@import "home.less"; +@import "list.less"; +@import "compose_mail.less"; +@import "view_mail.less"; diff --git a/email_app/app/reducers/create_account.js b/email_app/app/reducers/create_account.js index 2aa7636..80a2df6 100644 --- a/email_app/app/reducers/create_account.js +++ b/email_app/app/reducers/create_account.js @@ -2,8 +2,8 @@ import ACTION_TYPES from '../actions/actionTypes'; const initialState = { processing: false, - accounts: [], - error: {} + error: {}, + newAccount: null }; const createAccount = (state = initialState, action) => { @@ -12,10 +12,7 @@ const createAccount = (state = initialState, action) => { return { ...state, processing: true }; break; case `${ACTION_TYPES.CREATE_ACCOUNT}_SUCCESS`: { - let updatedCoreData = { ...state.coreData, id: action.payload }; - let updatedAccounts = state.accounts.push(action.payload); - return { ...state, coreData: updatedCoreData, - accounts: updatedAccounts, processing: false }; + return { ...state, processing: false, newAccount: action.payload }; break; } case ACTION_TYPES.CREATE_ACCOUNT_ERROR: diff --git a/email_app/app/reducers/initialiser.js b/email_app/app/reducers/initialiser.js index d3e633c..47ffc13 100644 --- a/email_app/app/reducers/initialiser.js +++ b/email_app/app/reducers/initialiser.js @@ -1,18 +1,18 @@ import ACTION_TYPES from '../actions/actionTypes'; -import { MESSAGES } from '../constants'; +import { MESSAGES, APP_STATUS } from '../constants'; const initialState = { - app: '', + app_status: null, + app: null, tasks: [], accounts: [], - config: null, coreData: { id: '', inbox: [], - saved: [], - outbox: [] + saved: [] }, - inboxSize: 0 + inboxSize: 0, + savedSize: 0 }; const initializer = (state = initialState, action) => { @@ -23,49 +23,54 @@ const initializer = (state = initialState, action) => { return { ...state, tasks }; break; } - case `${ACTION_TYPES.AUTHORISE_APP}_LOADING`: { - const tasks = state.tasks.slice(); - tasks.push(MESSAGES.INITIALIZE.AUTHORISE_APP); - return { ...state, tasks }; + case `${ACTION_TYPES.AUTHORISE_APP}_LOADING`: + return { ...state, app: null, app_status: APP_STATUS.AUTHORISING }; break; - } case `${ACTION_TYPES.AUTHORISE_APP}_SUCCESS`: - return { ...state, app: action.payload }; + return { ...state, app: action.payload, app_status: APP_STATUS.AUTHORISED }; break; - case `${ACTION_TYPES.GET_CONFIG}_LOADING`: { - const tasks = state.tasks.slice(); - tasks.push(MESSAGES.INITIALIZE.CHECK_CONFIGURATION); - return { ...state, tasks }; + case `${ACTION_TYPES.AUTHORISE_APP}_ERROR`: + return { ...state, app_status: APP_STATUS.AUTHORISATION_FAILED }; + break; + case `${ACTION_TYPES.GET_CONFIG}_LOADING`: + return { ...state, app_status: APP_STATUS.READING_CONFIG }; break; - } case `${ACTION_TYPES.GET_CONFIG}_SUCCESS`: - return { ...state, accounts: action.payload }; + return { ...state, + accounts: action.payload, + coreData: { ...state.coreData, id: action.payload.id }, + app_status: APP_STATUS.READY + }; + break; + case `${ACTION_TYPES.STORE_NEW_ACCOUNT}_SUCCESS`: + return { ...state, + accounts: action.payload, + coreData: { ...state.coreData, id: action.payload.id } + }; break; -/* case `${ACTION_TYPES.REFRESH_EMAIL}_SUCCESS`: - return { ...state, accounts: action.payload.data }; + case `${ACTION_TYPES.REFRESH_EMAIL}_LOADING`: + return { ...state, + coreData: { ...state.coreData, inbox: [], saved: [] }, + inboxSize: 0, + savedSize: 0 + }; break; case ACTION_TYPES.PUSH_TO_INBOX: { - const inbox = state.coreData.inbox.slice(); - inbox.push(action.data); - - return { - ...state, - coreData: { - ...state.coreData, - inbox - } + let inbox = Object.assign({}, state.coreData.inbox, action.payload); + return { ...state, + coreData: { ...state.coreData, inbox }, + inboxSize: Object.keys(inbox).length }; break; } - case ACTION_TYPES.CLEAR_INBOX: { - return { - ...state, - coreData: { - ...state.coreData, - inbox: [] - } + case ACTION_TYPES.PUSH_TO_ARCHIVE: { + let saved = Object.assign({}, state.coreData.saved, action.payload); + return { ...state, + coreData: { ...state.coreData, saved }, + savedSize: Object.keys(saved).length }; - }*/ + break; + } default: return state; break; diff --git a/email_app/app/safenet_comm.js b/email_app/app/safenet_comm.js new file mode 100644 index 0000000..7554c7d --- /dev/null +++ b/email_app/app/safenet_comm.js @@ -0,0 +1,256 @@ +import { CONSTANTS, MESSAGES } from './constants'; +import { initializeApp, fromAuthURI } from 'safe-app'; +import { getAuthData, saveAuthData, clearAuthData, hashPublicId, genRandomEntryKey, + genKeyPair, encrypt, decrypt, genServiceInfo } from './utils/app_utils'; +import pkg from '../package.json'; + +const APP_INFO = { + info: { + id: pkg.identifier, + scope: null, + name: pkg.productName, + vendor: pkg.vendor + }, + opts: { + own_container: true + }, + permissions: { + _publicNames: ['Read', 'Insert', 'Update'] + } +}; + +const requestAuth = () => { + return initializeApp(APP_INFO.info) + .then((app) => app.auth.genAuthUri(APP_INFO.permissions, APP_INFO.opts) + .then((resp) => app.auth.openUri(resp.uri)) + ); +} + +export const authApp = () => { + if (process.env.SAFE_FAKE_AUTH) { + return initializeApp(APP_INFO.info) + .then((app) => app.auth.loginForTest(APP_INFO.permissions)); + } + + let uri = getAuthData(); + if (uri) { + return fromAuthURI(APP_INFO.info, uri) + .then((registered_app) => registered_app.auth.refreshContainerAccess() + .then(() => registered_app) + .catch((err) => { + console.warn("Auth URI stored is not valid anymore, app needs to be re-authorised."); + clearAuthData(); + return requestAuth(); + }) + ); + } + + return requestAuth(); +} + +export const connect = (uri) => { + return fromAuthURI(APP_INFO.info, uri) + .then((app) => { + saveAuthData(uri); + return app; + }); +} + +export const readConfig = (app) => { + let account = {}; + return app.auth.refreshContainerAccess() + .then(() => app.auth.getHomeContainer()) + .then((md) => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_INBOX).then((key) => md.get(key)) + .then((value) => app.mutableData.fromSerial(value.buf)) + .then((inbox_md) => account.inbox_md = inbox_md) + .then(() => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_ARCHIVE).then((key) => md.get(key))) + .then((value) => app.mutableData.fromSerial(value.buf)) + .then((archive_md) => account.archive_md = archive_md) + .then(() => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_ID).then((key) => md.get(key))) + .then((value) => account.id = value.buf.toString()) + .then(() => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_ENC_SECRET_KEY).then((key) => md.get(key))) + .then((value) => account.enc_sk = value.buf.toString()) + .then(() => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY).then((key) => md.get(key))) + .then((value) => account.enc_pk = value.buf.toString()) + ) + .then(() => account); +} + +export const writeConfig = (app, account) => { + let serialised_inbox; + let serialised_archive; + return account.inbox_md.serialise() + .then((serial) => serialised_inbox = serial) + .then(() => account.archive_md.serialise()) + .then((serial) => serialised_archive = serial) + .then(() => app.auth.refreshContainerAccess()) + .then(() => app.auth.getHomeContainer()) + .then((md) => app.mutableData.newMutation() + .then((mut) => mut.insert(CONSTANTS.MD_KEY_EMAIL_INBOX, serialised_inbox) + .then(() => mut.insert(CONSTANTS.MD_KEY_EMAIL_ARCHIVE, serialised_archive)) + .then(() => mut.insert(CONSTANTS.MD_KEY_EMAIL_ID, account.id)) + .then(() => mut.insert(CONSTANTS.MD_KEY_EMAIL_ENC_SECRET_KEY, account.enc_sk)) + .then(() => mut.insert(CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY, account.enc_pk)) + .then(() => md.applyEntriesMutation(mut)) + )) + .then(() => account); +} + +export const readInboxEmails = (app, account, cb) => { + return account.inbox_md.getEntries() + .then((entries) => entries.forEach((key, value) => { + if (key.toString() !== CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY + && value.buf.toString().length > 0) { //FIXME: this condition is a work around for a limitation in safe_core + let entry_value = decrypt(value.buf.toString(), account.enc_sk, account.enc_pk); + return app.immutableData.fetch(Buffer.from(entry_value, 'hex')) + .then((immData) => immData.read()) + .then((content) => { + let decryptedEmail; + decryptedEmail = JSON.parse(decrypt(content.toString(), account.enc_sk, account.enc_pk)); + cb({ [key]: decryptedEmail }); + }) + } + }) + ); +} + +export const readArchivedEmails = (app, account, cb) => { + return account.archive_md.getEntries() + .then((entries) => entries.forEach((key, value) => { + if (value.buf.toString().length > 0) { //FIXME: this condition is a work around for a limitation in safe_core + return app.immutableData.fetch(value.buf) + .then((immData) => immData.read()) + .then((content) => { + let decryptedEmail; + decryptedEmail = JSON.parse(decrypt(content.toString(), account.enc_sk, account.enc_pk)); + cb({ [key]: decryptedEmail }); + }) + } + }) + ); +} + +const createInbox = (app, enc_pk) => { + let base_inbox = { + [CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY]: enc_pk + }; + let inbox_md; + let permSet; + + return app.mutableData.newRandomPublic(CONSTANTS.TAG_TYPE_INBOX) + .then((md) => md.quickSetup(base_inbox)) + .then((md) => inbox_md = md) + .then(() => app.mutableData.newPermissionSet()) + .then((pmSet) => permSet = pmSet) + .then(() => permSet.setAllow('Insert')) + .then(() => inbox_md.setUserPermissions(null, permSet, 1)) + .then(() => inbox_md); +} + +const createArchive = (app) => { + return app.mutableData.newRandomPrivate(CONSTANTS.TAG_TYPE_EMAIL_ARCHIVE) + .then((md) => md.quickSetup()); +} + +const addEmailService = (app, serviceInfo, inbox_serialised) => { + return app.mutableData.newPublic(serviceInfo.serviceAddr, CONSTANTS.TAG_TYPE_DNS) + .then((md) => md.quickSetup({ [serviceInfo.serviceName]: inbox_serialised })) +} + +const createPublicIdAndEmailService = (app, pub_names_md, serviceInfo, + inbox_serialised) => { + return addEmailService(app, serviceInfo, inbox_serialised) + .then((md) => md.getNameAndTag()) + .then((services) => app.mutableData.newMutation() + .then((mut) => mut.insert(serviceInfo.publicId, services.name) + .then(() => pub_names_md.applyEntriesMutation(mut)) + )) +} + +export const setupAccount = (app, emailId) => { + let newAccount = {}; + let inbox_serialised; + let inbox; + let key_pair = genKeyPair(); + let serviceInfo = genServiceInfo(emailId); + + return createInbox(app, key_pair.publicKey) + .then((md) => inbox = md) + .then(() => createArchive(app)) + .then((md) => newAccount = {id: serviceInfo.emailId, inbox_md: inbox, archive_md: md, + enc_sk: key_pair.privateKey, enc_pk: key_pair.publicKey}) + .then(() => newAccount.inbox_md.serialise()) + .then((md_serialised) => inbox_serialised = md_serialised) + .then(() => app.auth.refreshContainerAccess()) + .then(() => app.auth.getAccessContainerInfo('_publicNames')) + .then((pub_names_md) => pub_names_md.encryptKey(serviceInfo.publicId) + .then((encrypted_publicId) => pub_names_md.get(encrypted_publicId)) + .then((services) => addService(app, serviceInfo, inbox_serialised) + , (err) => { + if (err.name === 'ERR_NO_SUCH_ENTRY') { + return createPublicIdAndEmailService(app, pub_names_md, + serviceInfo, inbox_serialised); + } + throw err; + }) + ) + .then(() => newAccount); +} + +const writeEmailContent = (app, email, pk) => { + const encryptedEmail = encrypt(JSON.stringify(email), pk); + + return app.immutableData.create() + .then((email) => email.write(encryptedEmail) + .then(() => email.close()) + ); +} + +export const storeEmail = (app, email, to) => { + let serviceInfo = genServiceInfo(to); + return app.mutableData.newPublic(serviceInfo.serviceAddr, CONSTANTS.TAG_TYPE_DNS) + .then((md) => md.get(serviceInfo.serviceName)) + .catch((err) => {throw MESSAGES.EMAIL_ID_NOT_FOUND}) + .then((service) => app.mutableData.fromSerial(service.buf)) + .then((inbox_md) => inbox_md.get(CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY) + .then((pk) => writeEmailContent(app, email, pk.buf.toString()) + .then((email_addr) => app.mutableData.newMutation() + .then((mut) => { + let entry_value = encrypt(email_addr.buffer.toString('hex'), pk.buf.toString()); + let entry_key = genRandomEntryKey(); + return mut.insert(entry_key, entry_value) + .then(() => inbox_md.applyEntriesMutation(mut)) + }) + ))); +} + +export const removeInboxEmail = (app, account, key) => { + return app.mutableData.newMutation() + .then((mut) => mut.remove(key, 1) + .then(() => account.inbox_md.applyEntriesMutation(mut)) + ) +} + +export const removeArchivedEmail = (app, account, key) => { + return app.mutableData.newMutation() + .then((mut) => account.archive_md.encryptKey(key) + .then((encryptedKey) => mut.remove(encryptedKey, 1)) + .then(() => account.archive_md.applyEntriesMutation(mut)) + ) +} + +export const archiveEmail = (app, account, key) => { + let new_entry_key = genRandomEntryKey(); + return account.inbox_md.get(key) + .then((encryptedEmailXorName) => decrypt(encryptedEmailXorName.buf.toString(), account.enc_sk, account.enc_pk)) + .then((decrytedXorNameHex) => Buffer.from(decrytedXorNameHex, 'hex')) + .then((xorName) => app.mutableData.newMutation() + .then((mut) => mut.insert(new_entry_key, xorName) + .then(() => account.archive_md.applyEntriesMutation(mut)) + ) + ) + .then(() => app.mutableData.newMutation()) + .then((mut) => mut.remove(key, 1) + .then(() => account.inbox_md.applyEntriesMutation(mut)) + ) +} diff --git a/email_app/app/utils/app_utils.js b/email_app/app/utils/app_utils.js index 63c8499..7722cee 100644 --- a/email_app/app/utils/app_utils.js +++ b/email_app/app/utils/app_utils.js @@ -2,6 +2,7 @@ import crypto from 'crypto'; import * as base64 from 'urlsafe-base64'; import { remote } from 'electron'; import { CONSTANTS } from '../constants'; +import sodium from 'libsodium-wrappers'; export const getAuthData = () => { let authData = window.JSON.parse( @@ -10,34 +11,45 @@ export const getAuthData = () => { return authData; }; -export const setAuthData = (authData) => { +export const saveAuthData = (authData) => { return window.localStorage.setItem(CONSTANTS.LOCAL_AUTH_DATA_KEY, window.JSON.stringify(authData) ); }; -export const checkAuthorised = (nextState, replace, callback) => { - let authData = getAuthData(); - if (!authData) { - replace('/create_account'); - } - callback(); +export const clearAuthData = () => { + window.localStorage.removeItem(CONSTANTS.LOCAL_AUTH_DATA_KEY); }; -export const hashEmailId = emailId => { - return crypto.createHash('sha256').update(emailId).digest('base64'); +export const genServiceInfo = (emailId) => { + // It supports complex email IDs, e.g. 'emailA.myshop', 'emailB.myshop' + let str = emailId.replace(/\.+$/, ''); + let toParts = str.split('.'); + const publicId = toParts.pop(); + const serviceId = str.slice(0, -1 * (publicId.length+1)); + emailId = (serviceId.length > 0 ? (serviceId + '.') : '') + publicId; + const serviceName = serviceId + CONSTANTS.SERVICE_NAME_POSTFIX; + const serviceAddr = hashPublicId(publicId); + return {emailId, publicId, serviceAddr, serviceName}; +} + +export const hashPublicId = publicId => { + return crypto.createHash('sha256').update(publicId).digest(); +}; + +export const genRandomEntryKey = () => { + return crypto.randomBytes(32).toString('hex'); }; -export const showError = (title, errMsg) => { +export const showError = (title, errMsg, next) => { remote.dialog.showMessageBox({ type: 'error', buttons: ['Ok'], title, - message: errMsg - }, _ => {}); + message: errMsg.toString() + }, next ? next : _ => {}); }; - export const showSuccess = (title, message) => { remote.dialog.showMessageBox({ type: 'info', @@ -46,3 +58,16 @@ export const showSuccess = (title, message) => { message }, _ => {}); }; + +export const genKeyPair = () => { + let {keyType, privateKey, publicKey} = sodium.crypto_box_keypair('hex'); + return {privateKey, publicKey}; +} + +export const encrypt = (input, pk) => sodium.crypto_box_seal(input, Buffer.from(pk, 'hex'), 'hex'); + +export const decrypt = (cipherMsg, sk, pk) => sodium.crypto_box_seal_open( + Buffer.from(cipherMsg, 'hex'), + Buffer.from(pk, 'hex'), + Buffer.from(sk, 'hex'), + 'text'); diff --git a/email_app/design/EmailApp-DataModel.png b/email_app/design/EmailApp-DataModel.png new file mode 100644 index 0000000000000000000000000000000000000000..708c3b605b94afe3b5cd87714647cc1749e7d94f GIT binary patch literal 83136 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 i=d#Wzp$PybK}$#g literal 0 HcmV?d00001 diff --git a/email_app/design/EmailApp-DataModel.xml b/email_app/design/EmailApp-DataModel.xml new file mode 100644 index 0000000..4cb1627 --- /dev/null +++ b/email_app/design/EmailApp-DataModel.xml @@ -0,0 +1 @@ +7Vzdc6o4FP9rnO59sBM+hcdW294+9M7OdGf2vjkBgrIFwga0un/9Bggf4UPRWosWHxBPQnLOycnJOb8ER9LU2zwRGCxfsIXckQiszUiajURRBLJKv2LKllFUOSUsiGOlJKEgvDr/IUYEjLpyLBRyFSOM3cgJeKKJfR+ZEUeDhOB3vpqNXb7XAC5Yj6AgvJrQzfi4VQr6344VLVO6JqoF/SdyFsusb0HV0xIDmm8Lglc+63EkSnbySYs9mLXFeg6X0MLvJZL0MJKmBOMovfM2U+TG2s0Ulz0XbTNuR9L9MvJc+kOgt0nxY8vDQpeHqXQE+VG5u9b2AGNnDd1V1qKouvTh+4DrSP13FYt070GycPyRdEdL5WBDr6B0TVgAEdpEY+g6C1bPpNwgUrRB7xbsO+nJaOwpacVCJiYwcjBrio4LIq7jo3prz76BN0mttFUqtVH0BP5cGa5j0puXVQQNF81gBEtVgypTS1Kl7FdIWQkpfewiO0oLtbiwTQU2pgNGLR2afOtTvCJOrDvwC703yDxL+kuuogq9gNKS9gj0LezlNK4vKm3anVol15TQQ5GjbYDmEVzkggsKANJ1yBYg4jlhSK09LMZVuZKBy43RN8Ig7bdOyh569kNEonIj9cpJ35x9gzt/ixPfcA36UmbXIQj1/rT4u1v0fI486Lhz5Jvz4O1gy+Z6LzNtYhcTjmsasgD6iUOWHStuSpgYgqxOJiJUoK0qgq6LigIlQ1YMw5Jt0RJ0RYUT0dQUfWJDJFimolnA1BVTEAV7Up5qRsMoVgY352baMwMIA+g3Nh8zN7ah57jbtJl8YKiWJZl+m7mN+ImN8KXHGk33pTxlvT/i1G23JlxPWKczkWyD6I9a8LSHRedM/D17XjlUBb8x+QW98pRz+jDwmYf6wU/rJs0N03qY1sO0vrhp/W3nNBeB7xe+As2saQbjmNC9S5GQWYTj7hguMkvFvce0lu0mCJLtuC6Tg0Fqgsh+PzKxZj+Ru0ZxqyMO93Ghgdz7HLqaspjQj5MhqrKI4Dc0zQNFSaWfx8ec5zJexBCmmHe0KZEYfvSEsIcisqVVGDY4AWL6yHuBs6kqg5WWZYxN1xjCx9C9Rd5WgVDRGwZStQFWwgcAqwarSwalALPSsjYAq1x8ADKVMZhHyD+p4FM6rJBWJyVbagqhLxOY2pHzGTX+e5IEnCMLTHNAas0787/OpE6JYs2qdmeOug5AXUpvm7AutIzZbZe2TZM6hsa2x0GCzo4TxeyxibI4vU0oj7el0iJ3vH0lUHjNEHbGZgd01mZ3pVgrRMShDjREdEDBTcLPDb17qUeR3TnrkF0MQw+JuXTW/CL0kcE/cKRZ981jvWMAO4g5jDEPIoYVEPEkGVrds3Z+SFQETVNtVaERngF06u4tDQq2rEJJBool6ppliZqENKCqmgY0GyoatGRdlAXNsDTts/kbHMgBxlVFqL/auCq6uSTQ+yx6aQyqr9DGD9wT+14ZOSsdqxpLv7f5KQ/QMUdXpFPk6PLVHCq5Y+FU+7ES4qxhhEbDuZILdy2N50rk65Ct/zvww07NN9yp4dW8h69he4ZTWceUpjEuvASXxT9+2pl79Hxp8pSN6VZHg/6gxTWbxCWE/8Mm3slThlp+0JBFtG7i6Uo9QRD1pgRBAqdIEJSrSRAeYsikNT3gPUllovclR/iisxmTfb7q69euHQjUJ+wS2oSuVbyeO+P+wi2OloiwfbRnbn+xw95Yfw52nEvZ4cr4J3n1iOm7rMxYSFZ8eWo8L3ocOR46wmZfYZQoFCTLwF0Qs0Uz7kmcdlPPcSeL8UWnP59e/mobhAEbPcUIGtiqevfOXoetuocM0NWd/pjEgMqPI4SxnHUn5XD1vld0yh6QBHCraqo4kRWga0DSOXS7KXaVZP3TYtfJ1cSuN/M0XIizzTDesr+QM2lnDVh3Iaf9jFrPcY6t0tdBx71KRpwSWs5/NVle2wtFnRavvWdZcuQFYJtebsqMxfMjRGTtmIk5mKWp0orI9WFd6x2yzh4q5ytj/kBk7/Dpow2IE/K5sw31KqHgeBjAtCP235uOyCtyE7oGThKhaNcToRQeOJ09r9ns+bb/8xAuoaio7O2misusrZhfdBr7qHyoh2pvPBPQdgT+wmQ77r8merxx01tN9//0RV8yhraXT2r+reFtlA7ZQsVHyoCBWLud2aExYOdXH4ZMoZvlvL/zttfX7OAjL3J9yms3VHFHv3RzJhX28x8DhmTn+GRHy1DWMhzbeJQgw20PSHboz+Kv9ZKyp+IfDKWH/wE= \ No newline at end of file diff --git a/email_app/package.json b/email_app/package.json index 9105f9c..3c6be58 100644 --- a/email_app/package.json +++ b/email_app/package.json @@ -1,10 +1,10 @@ { "name": "safe-mail-tutorial", - "productName": "SAFE Mail Tutorial", - "version": "0.1.2", + "productName": "SAFE-Mail-Tutorial", + "version": "0.2.0", "description": "Mailing application tutorial using SAFE Network", - "identifier": "net.maidsafe.mailtutorial", - "vendor": "MaidSafe Ltd.", + "identifier": "net.maidsafe.examples.mailtutorial", + "vendor": "MaidSafe", "main": "app/index.js", "scripts": { "test": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 mocha --compilers js:babel-register --recursive --require ./test/setup.js test/**/*.spec.js", @@ -28,7 +28,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/shankar2105/safe_mail_app.git" + "url": "git+https://github.com/maidsafe/safe_examples.git" }, "author": { "name": "MaidSafe", @@ -37,12 +37,12 @@ }, "license": "MIT", "bugs": { - "url": "https://github.com/shankar2105/safe_mail_app/issues" + "url": "https://github.com/maidsafe/safe_examples/issues" }, "keywords": [ "maidsafe" ], - "homepage": "https://github.com/shankar2105/safe_mail_app#readme", + "homepage": "https://github.com/maidsafe/safe_examples/blob/master/email_app/README.md", "devDependencies": { "asar": "^0.12.2", "babel-core": "^6.14.0", @@ -61,7 +61,6 @@ "babel-preset-stage-0": "^6.5.0", "babel-register": "^6.14.0", "chai": "^3.5.0", - "classnames": "^2.2.5", "concurrently": "^2.2.0", "cross-env": "^2.0.0", "css-loader": "^0.24.0", @@ -102,10 +101,13 @@ "axios": "^0.14.0", "babel-plugin-transform-class-properties": "^6.23.0", "buffer-stream-reader": "^0.1.1", + "classnames": "^2.2.5", "css-modules-require-hook": "^4.0.2", "dateformat": "^1.0.12", - "electron-compile": "^6.1.3", + "electron-compile": "^6.2.0", "electron-debug": "^1.0.1", + "less": "^2.7.2", + "libsodium-wrappers": "^0.5.1", "material-design-lite": "^1.2.1", "open-sans-fontface": "^1.4.0", "postcss": "^5.1.2", @@ -141,7 +143,12 @@ "rpm" ] }, - "electronPackagerConfig": {}, + "electronPackagerConfig": { + "name": "safe-mail-tutorial", + "appBundleId": "net.maidsafe.examples.mailtutorial", + "appCategoryType": "public.app-category.tools", + "dir": "./" + }, "electronWinstallerConfig": { "name": "" }, diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/controller/AppController.java b/email_app_java/src/main/java/net/maidsafe/example/mail/controller/AppController.java index 89cbf02..0d2704c 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/controller/AppController.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/controller/AppController.java @@ -3,8 +3,8 @@ import java.util.List; import net.maidsafe.example.mail.dao.IMessagingDao; import net.maidsafe.example.mail.dao.LauncherAPI; -import net.maidsafe.example.mail.modal.Message; -import net.maidsafe.example.mail.modal.Result; +import net.maidsafe.example.mail.model.Message; +import net.maidsafe.example.mail.model.Result; import net.maidsafe.example.mail.util.ServiceHandler; /** diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/controller/IdCreationController.java b/email_app_java/src/main/java/net/maidsafe/example/mail/controller/IdCreationController.java index f1560d2..7ef4589 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/controller/IdCreationController.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/controller/IdCreationController.java @@ -2,7 +2,7 @@ import net.maidsafe.example.mail.dao.IMessagingDao; import net.maidsafe.example.mail.dao.LauncherAPI; -import net.maidsafe.example.mail.modal.Result; +import net.maidsafe.example.mail.model.Result; import net.maidsafe.example.mail.util.ServiceHandler; /** diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/controller/InitialisationController.java b/email_app_java/src/main/java/net/maidsafe/example/mail/controller/InitialisationController.java index 1960692..c356fef 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/controller/InitialisationController.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/controller/InitialisationController.java @@ -2,7 +2,7 @@ import net.maidsafe.example.mail.dao.IMessagingDao; import net.maidsafe.example.mail.dao.LauncherAPI; -import net.maidsafe.example.mail.modal.Result; +import net.maidsafe.example.mail.model.Result; import net.maidsafe.example.mail.util.ServiceHandler; /** diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/dao/IMessagingDao.java b/email_app_java/src/main/java/net/maidsafe/example/mail/dao/IMessagingDao.java index 7ce40ae..a2b74b1 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/dao/IMessagingDao.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/dao/IMessagingDao.java @@ -2,8 +2,8 @@ import java.util.List; import java.util.concurrent.Future; -import net.maidsafe.example.mail.modal.Message; -import net.maidsafe.example.mail.modal.Result; +import net.maidsafe.example.mail.model.Message; +import net.maidsafe.example.mail.model.Result; /** * diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/dao/IRestAPI.java b/email_app_java/src/main/java/net/maidsafe/example/mail/dao/IRestAPI.java index cf28266..3f5b506 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/dao/IRestAPI.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/dao/IRestAPI.java @@ -1,17 +1,17 @@ package net.maidsafe.example.mail.dao; -import net.maidsafe.example.mail.modal.AppConfig; -import net.maidsafe.example.mail.modal.AppendableDataCreateRequest; -import net.maidsafe.example.mail.modal.AppendableDataDataId; -import net.maidsafe.example.mail.modal.AppendableDataMetadata; -import net.maidsafe.example.mail.modal.AuthRequest; -import net.maidsafe.example.mail.modal.AuthResponse; -import net.maidsafe.example.mail.modal.HandleIdResponse; -import net.maidsafe.example.mail.modal.ReaderInfo; -import net.maidsafe.example.mail.modal.StructuredDataCreateRequest; -import net.maidsafe.example.mail.modal.StructuredDataDataId; -import net.maidsafe.example.mail.modal.StructuredDataMetadata; -import net.maidsafe.example.mail.modal.StructuredDataUpdateRequest; +import net.maidsafe.example.mail.model.AppConfig; +import net.maidsafe.example.mail.model.AppendableDataCreateRequest; +import net.maidsafe.example.mail.model.AppendableDataDataId; +import net.maidsafe.example.mail.model.AppendableDataMetadata; +import net.maidsafe.example.mail.model.AuthRequest; +import net.maidsafe.example.mail.model.AuthResponse; +import net.maidsafe.example.mail.model.HandleIdResponse; +import net.maidsafe.example.mail.model.ReaderInfo; +import net.maidsafe.example.mail.model.StructuredDataCreateRequest; +import net.maidsafe.example.mail.model.StructuredDataDataId; +import net.maidsafe.example.mail.model.StructuredDataMetadata; +import net.maidsafe.example.mail.model.StructuredDataUpdateRequest; import okhttp3.RequestBody; import okhttp3.ResponseBody; import retrofit2.Call; diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/dao/LauncherAPI.java b/email_app_java/src/main/java/net/maidsafe/example/mail/dao/LauncherAPI.java index e90689f..dcb82a7 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/dao/LauncherAPI.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/dao/LauncherAPI.java @@ -12,20 +12,20 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; -import net.maidsafe.example.mail.modal.AppConfig; -import net.maidsafe.example.mail.modal.AppInfo; -import net.maidsafe.example.mail.modal.AppendableDataCreateRequest; -import net.maidsafe.example.mail.modal.AppendableDataDataId; -import net.maidsafe.example.mail.modal.AppendableDataMetadata; -import net.maidsafe.example.mail.modal.AuthRequest; -import net.maidsafe.example.mail.modal.AuthResponse; -import net.maidsafe.example.mail.modal.FileResponse; -import net.maidsafe.example.mail.modal.HandleIdResponse; -import net.maidsafe.example.mail.modal.Message; -import net.maidsafe.example.mail.modal.Result; -import net.maidsafe.example.mail.modal.StructuredDataCreateRequest; -import net.maidsafe.example.mail.modal.StructuredDataDataId; -import net.maidsafe.example.mail.modal.StructuredDataUpdateRequest; +import net.maidsafe.example.mail.model.AppConfig; +import net.maidsafe.example.mail.model.AppInfo; +import net.maidsafe.example.mail.model.AppendableDataCreateRequest; +import net.maidsafe.example.mail.model.AppendableDataDataId; +import net.maidsafe.example.mail.model.AppendableDataMetadata; +import net.maidsafe.example.mail.model.AuthRequest; +import net.maidsafe.example.mail.model.AuthResponse; +import net.maidsafe.example.mail.model.FileResponse; +import net.maidsafe.example.mail.model.HandleIdResponse; +import net.maidsafe.example.mail.model.Message; +import net.maidsafe.example.mail.model.Result; +import net.maidsafe.example.mail.model.StructuredDataCreateRequest; +import net.maidsafe.example.mail.model.StructuredDataDataId; +import net.maidsafe.example.mail.model.StructuredDataUpdateRequest; import okhttp3.Headers; import okhttp3.HttpUrl; @@ -52,7 +52,7 @@ public class LauncherAPI implements IMessagingDao, Cloneable { private final String END_POINT = "http://localhost:8100/"; private IRestAPI api; - // Hanldes + // Handles private Long symmetricCipherOptsHandle; private long structuredDataHandle; private List savedMessagesDataIdList; @@ -195,7 +195,7 @@ public Future> createID(String id) { appendableDataHandle = res.body().getHandleId(); // Invoke PUT endpoint for creating AD api.saveAppendableData(authToken, appendableDataHandle).execute(); - // Create SD for saved messages bby passing symmetric cipher opts handle + // Create SD for saved messages by passing symmetric cipher opts handle sdCreateReq = new StructuredDataCreateRequest(appConfig.getStructuredDataName(), UNVERSIONED_SD_TAG, symmetricCipherOptsHandle, Base64.getEncoder().encodeToString("[]".getBytes())); structuredDataHandle = api.createStructuredData(authToken, sdCreateReq).execute().body().getHandleId(); api.saveStructuredData(authToken, structuredDataHandle).execute(); @@ -296,9 +296,9 @@ public Future>> fetchSavedMessages() { * Invoked to send a message The recipient's appendable data is fetched and * the encrypt key is obtained The message is written to the network as * ImmutableData and encrypted using the receiver's encrypt key (Asymmetric - * encryption) - * The DataId of the ImmutableData is added to the receiver's appendable data - * + * encryption) The DataId of the ImmutableData is added to the receiver's + * appendable data + * * @param message * @return */ @@ -309,7 +309,7 @@ public Future> sendMessage(Message message) { String toName; long toDataId; long toAppendableData; - long dataIdForMessage; + long dataIdForMessage; toName = getBase64Name(message.getTo()); toDataId = api.getDataId(authToken, new AppendableDataDataId(toName, true)).execute().body().getHandleId(); @@ -318,7 +318,7 @@ public Future> sendMessage(Message message) { return new Result(false, "Failed to get appendable data for recepient"); } toAppendableData = res.body().getHandleId(); - + long encryptKeyHandle = api.getEncryptKey(authToken, toAppendableData).execute().body().getHandleId(); long cipherHandle = api.getAsymmetricCipherOptHandle(authToken, encryptKeyHandle).execute().body().getHandleId(); long writerHandle = api.getImmutableDataWritter(authToken).execute().body().getHandleId(); @@ -337,20 +337,20 @@ public Future> sendMessage(Message message) { api.dropImmutableDataWriter(authToken, writerHandle).execute(); api.dropDataId(authToken, dataIdForMessage).execute(); api.dropDataId(authToken, toDataId).execute(); - api.dropAppendableDataHandle(authToken, toAppendableData); + api.dropAppendableDataHandle(authToken, toAppendableData); return new Result(true, true); }); } /** - * Save the message from inbox to the Structured Data - * Fetch the DataId of the message from the appendable data - * Read the message and save it as symmetric encrypted ImmutableData - * The DataId of the ImmutableData is serialised and stored in the SD - * The message is also removed from the AppendableData - * + * Save the message from inbox to the Structured Data Fetch the DataId of + * the message from the appendable data Read the message and save it as + * symmetric encrypted ImmutableData The DataId of the ImmutableData is + * serialised and stored in the SD The message is also removed from the + * AppendableData + * * @param message - * @return + * @return */ @Override public Future> saveMessage(Message message) { diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/AppConfig.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/AppConfig.java similarity index 94% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/AppConfig.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/AppConfig.java index e0f8d65..83b438a 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/AppConfig.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/AppConfig.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; /** * The information needed by the application to bootstrap is stored in the config file diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/AppInfo.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/AppInfo.java similarity index 94% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/AppInfo.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/AppInfo.java index 260a287..173e4bc 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/AppInfo.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/AppInfo.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; /** * diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/AppendableDataCreateRequest.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/AppendableDataCreateRequest.java similarity index 91% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/AppendableDataCreateRequest.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/AppendableDataCreateRequest.java index db3f987..fe0403c 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/AppendableDataCreateRequest.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/AppendableDataCreateRequest.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; /** * diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/AppendableDataDataId.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/AppendableDataDataId.java similarity index 90% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/AppendableDataDataId.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/AppendableDataDataId.java index ce049a0..83171e8 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/AppendableDataDataId.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/AppendableDataDataId.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; /** * diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/AppendableDataMetadata.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/AppendableDataMetadata.java similarity index 97% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/AppendableDataMetadata.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/AppendableDataMetadata.java index ed2430d..c4b60ae 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/AppendableDataMetadata.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/AppendableDataMetadata.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; /** * diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/AuthRequest.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/AuthRequest.java similarity index 91% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/AuthRequest.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/AuthRequest.java index 89412cf..75651c1 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/AuthRequest.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/AuthRequest.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; import java.util.ArrayList; import java.util.List; diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/AuthResponse.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/AuthResponse.java similarity index 92% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/AuthResponse.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/AuthResponse.java index 24cfcd9..532839b 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/AuthResponse.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/AuthResponse.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; import java.util.List; diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/FileResponse.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/FileResponse.java similarity index 93% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/FileResponse.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/FileResponse.java index 8027459..d11e8ca 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/FileResponse.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/FileResponse.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; /** * diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/HandleIdResponse.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/HandleIdResponse.java similarity index 86% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/HandleIdResponse.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/HandleIdResponse.java index be8f747..fab9c8d 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/HandleIdResponse.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/HandleIdResponse.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; /** diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/Message.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/Message.java similarity index 96% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/Message.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/Message.java index e39d3bb..980f2be 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/Message.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/Message.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; import java.util.Date; diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/ReaderInfo.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/ReaderInfo.java similarity index 90% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/ReaderInfo.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/ReaderInfo.java index 59fcbe2..7578a2b 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/ReaderInfo.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/ReaderInfo.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; /** * diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/Result.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/Result.java similarity index 93% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/Result.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/Result.java index dcf1b7d..a894d5a 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/Result.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/Result.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; /** * diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/StructuredDataCreateRequest.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/StructuredDataCreateRequest.java similarity index 96% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/StructuredDataCreateRequest.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/StructuredDataCreateRequest.java index e7fb092..6211681 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/StructuredDataCreateRequest.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/StructuredDataCreateRequest.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; /** * diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/StructuredDataDataId.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/StructuredDataDataId.java similarity index 90% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/StructuredDataDataId.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/StructuredDataDataId.java index 0b4d00c..e8f578d 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/StructuredDataDataId.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/StructuredDataDataId.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; /** * diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/StructuredDataMetadata.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/StructuredDataMetadata.java similarity index 95% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/StructuredDataMetadata.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/StructuredDataMetadata.java index cf25a27..2affdc9 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/StructuredDataMetadata.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/StructuredDataMetadata.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; /** * diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/StructuredDataUpdateRequest.java b/email_app_java/src/main/java/net/maidsafe/example/mail/model/StructuredDataUpdateRequest.java similarity index 91% rename from email_app_java/src/main/java/net/maidsafe/example/mail/modal/StructuredDataUpdateRequest.java rename to email_app_java/src/main/java/net/maidsafe/example/mail/model/StructuredDataUpdateRequest.java index a40ebd7..bb60659 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/modal/StructuredDataUpdateRequest.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/model/StructuredDataUpdateRequest.java @@ -1,4 +1,4 @@ -package net.maidsafe.example.mail.modal; +package net.maidsafe.example.mail.model; /** * diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/scene/HomeScene.java b/email_app_java/src/main/java/net/maidsafe/example/mail/scene/HomeScene.java index 646cb01..d891308 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/scene/HomeScene.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/scene/HomeScene.java @@ -28,7 +28,7 @@ import javafx.scene.text.FontWeight; import javafx.stage.Stage; import net.maidsafe.example.mail.controller.AppController; -import net.maidsafe.example.mail.modal.Message; +import net.maidsafe.example.mail.model.Message; /** * diff --git a/email_app_java/src/main/java/net/maidsafe/example/mail/util/ServiceHandler.java b/email_app_java/src/main/java/net/maidsafe/example/mail/util/ServiceHandler.java index 66d6f42..5f47518 100644 --- a/email_app_java/src/main/java/net/maidsafe/example/mail/util/ServiceHandler.java +++ b/email_app_java/src/main/java/net/maidsafe/example/mail/util/ServiceHandler.java @@ -4,7 +4,7 @@ import javafx.application.Platform; import javafx.concurrent.Service; import javafx.concurrent.Task; -import net.maidsafe.example.mail.modal.Result; +import net.maidsafe.example.mail.model.Result; /** * Blocking operations are threaded and once the result is obtained the diff --git a/markdown_editor/src/store.js b/markdown_editor/src/store.js index fbdb0c6..f08ea70 100644 --- a/markdown_editor/src/store.js +++ b/markdown_editor/src/store.js @@ -51,16 +51,7 @@ const _refreshConfig = () => { }; const getSDHandle = (filename) => { - let dataIdHandle = null; - return safeDataId.getStructuredDataHandle(ACCESS_TOKEN, btoa(`${USER_PREFIX}:${filename}`), 501) - .then(extractHandle) - .then(handleId => (dataIdHandle = handleId)) - .then(() => safeStructuredData.getHandle(ACCESS_TOKEN, dataIdHandle)) - .then(extractHandle) - .then(handleId => { - safeDataId.dropHandle(ACCESS_TOKEN, dataIdHandle); - return handleId; - }) + return Promise.resolve(FILE_INDEX[filename]); }; const updateFile = (filename, payload) => { diff --git a/web_hosting_manager/README.md b/web_hosting_manager/README.md index 9693b42..5041c9a 100644 --- a/web_hosting_manager/README.md +++ b/web_hosting_manager/README.md @@ -1,5 +1,8 @@ # SAFE Hosting Manager +#### Prerequisites +> SAFE Hosting Manager uses **[keytar](https://www.npmjs.com/package/keytar)** module as its dependency. Please install the prerequisites mentioned [here](https://www.npmjs.com/package/keytar#installing) based on the platform. + ## Install * **Note: requires a node version 6.5.0 and an npm version 3.10.3** @@ -7,17 +10,20 @@ First, clone the repo via git: ```bash -git clone https://github.com/maidsafe/safe_examples && cd safe_examples/web_hosting_manager +$ git clone https://github.com/maidsafe/safe_examples && cd safe_examples/web_hosting_manager ``` -And then install dependencies. -Install with [yarn](https://github.com/yarnpkg/yarn) for faster and safer installation +And then install Node.js dependencies. ```bash -yarn install +$ npm i ``` -Manually build [safe_app_nodejs](https://github.com/maidsafe/safe_app_nodejs) dependency from `app/node_modules/safe-app` +Finally, rebuild the native modules + +```bash +$ npm run rebuild +``` ## Run @@ -34,36 +40,8 @@ or run two servers with one command $ npm run dev ``` -## CSS Modules - -This boilerplate out of the box is configured to use [css-modules](https://github.com/css-modules/css-modules) and SASS. - -All `.scss` file extensions will use css-modules unless it has `.global.scss`. - -If you need global styles, stylesheets with `.global.scss` will not go through the -css-modules loader. e.g. `app.global.scss` - -If you want to import global css libraries (like `bootstrap`), you can just write the following code in `.global.scss`: - -```css -@import "~bootstrap/dist/css/bootstrap.css"; -``` - -For SASS mixin -```css -@import "~bootstrap/dist/css/bootstrap.css"; -``` - - ## Packaging -Based on the platform configure `build.asarUnpack` option in package.json -``` -osx : "*.dylib" -linux : "*.so" -windows: "*.dll" -``` - To package apps for the local platform: ```bash @@ -72,6 +50,10 @@ $ npm run package To package apps for all platforms: +```bash +$ npm run package-all +``` + To package apps with options: ```bash @@ -87,17 +69,18 @@ $ npm run build $ npm start ``` -To run End-to-End Test +# License -```bash -$ npm run build -$ npm run test-e2e -``` +Licensed under either of + +* the MaidSafe.net Commercial License, version 1.0 or later ([LICENSE](LICENSE)) +* the General Public License (GPL), version 3 ([COPYING](COPYING) or http://www.gnu.org/licenses/gpl-3.0.en.html) -#### Module Structure +at your option. -This boilerplate uses a [two package.json structure](https://github.com/electron-userland/electron-builder#two-packagejson-structure). +# Contribution -1. If the module is native to a platform or otherwise should be included with the published package (i.e. bcrypt, openbci), it should be listed under `dependencies` in `./app/package.json`. -2. If a module is `import`ed by another module, include it in `dependencies` in `./package.json`. See [this ESLint rule](https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-extraneous-dependencies.md). -3. Otherwise, modules used for building, testing and debugging should be included in `devDependencies` in `./package.json`. +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the +work by you, as defined in the MaidSafe Contributor Agreement, version 1.1 ([CONTRIBUTOR] +(CONTRIBUTOR)), shall be dual licensed as above, and you agree to be bound by the terms of the +MaidSafe Contributor Agreement, version 1.1. diff --git a/web_hosting_manager/app/components/CreateService.js b/web_hosting_manager/app/components/CreateService.js index 14a951f..ca8333b 100644 --- a/web_hosting_manager/app/components/CreateService.js +++ b/web_hosting_manager/app/components/CreateService.js @@ -121,7 +121,7 @@ export default class CreateService extends Component {
    - +
    ) } -} \ No newline at end of file +} diff --git a/markdown_editor/src/components/version_diff.js b/markdown_editor/src/components/version_diff.js index b7f232b..8813c81 100644 --- a/markdown_editor/src/components/version_diff.js +++ b/markdown_editor/src/components/version_diff.js @@ -79,41 +79,41 @@ export default class VersionDiff extends Component {
    { this.state.showComp ? ( -
    - -
    - - +
    + +
    + + +
    -
    - ) : '' + ) : '' }
    { this.props.versions.length > 1 ? ( - - ) : '' + + ) : '' }
    @@ -121,9 +121,9 @@ export default class VersionDiff extends Component {
    { (this.state.compA !== -1 && this.state.compB !== -1) ? : '' + inputA={this.props.versions[this.state.compA].content} + inputB={this.props.versions[this.state.compB].content} + type="chars"/> : '' }
    diff --git a/markdown_editor/src/config.js b/markdown_editor/src/config.js index 7356481..ef3d8bc 100644 --- a/markdown_editor/src/config.js +++ b/markdown_editor/src/config.js @@ -1,5 +1,5 @@ - // CONFIGURATION +import pkg from './../package.json'; // Theme for the editor // try 'elegant' or 'material' @@ -8,6 +8,16 @@ export const EDITOR_THEME = 'mdn-like'; // GLOBAL CONSTANTS export const APP_NAME = "SAFE Markdown Editor"; -export const APP_VERSION = '0.1'; +export const APP_VERSION = pkg.version; export const APP_ID = 'net.maidsafe.examples.markdown-editor'; +export const TYPE_TAG = 15463; +export const APP_INFO = { + id: APP_ID, + name: APP_NAME, + vendor: 'MaidSafe.net Ltd.', + scope: '' +}; +export const CONTAINERS = { + '_public': ['Read', 'Insert', 'Update', 'Delete', 'ManagePermissions'] +}; diff --git a/markdown_editor/src/store.js b/markdown_editor/src/store.js index f08ea70..7fc8150 100644 --- a/markdown_editor/src/store.js +++ b/markdown_editor/src/store.js @@ -1,190 +1,284 @@ /* global btoa, safeAuth, safeNFS safeCipherOpts, safeStructuredData, safeDataId */ +import crypto from 'crypto'; +import { APP_ID, APP_INFO, CONTAINERS, TYPE_TAG } from './config.js'; -// this is only file directly interacting with SAFE +const requiredWindowObj = [ + 'safeApp', + 'safeMutableData', + 'safeMutableDataEntries', + 'safeImmutableData', + 'safeMutableDataPermissionsSet', + 'safeMutableDataPermissions', + 'safeNfs' +]; -import { APP_ID, APP_NAME, APP_VERSION } from './config.js' +// check SAFE API are available +requiredWindowObj.forEach((obj) => { + if (!window.hasOwnProperty(obj)) { + throw new Error(`${obj} not found. Please check beaker-plugin-safe-app`); + } +}); -if (process.env.NODE_ENV !== 'production') { - require('safe-js/dist/polyfill') -} +const INDEX_FILE_NAME = crypto.createHash('sha256').update(`${window.location.host}-${APP_ID}`).digest('hex'); +const RES_URI_KEY = 'SAFE_RES_URI'; // global access state let ACCESS_TOKEN; -let SYMETRIC_CYPHER_HANDLE; -let INDEX_HANDLE; let FILE_INDEX; -let USER_PREFIX; -// legacy style fallback -const extractHandle = (res) => res.hasOwnProperty('handleId') ? res.handleId : res.__parsedResponseBody__.handleId; +/** + * Save response URI to local storage + * @param uri + * @private + */ +const _saveResponseUri = (uri) => { + if (typeof uri !== 'string') { + throw new Error('URI is not a String'); + } + window.localStorage.setItem(RES_URI_KEY, uri); +}; + +/** + * Get response URI from local storage + * @private + */ +const _getResponseUri = () => { + return window.localStorage.getItem(RES_URI_KEY); +}; -const _createRandomUserPrefix = () => { - let randomString = ''; - for (var i = 0; i < 10; i++) { - // and ten random ascii chars - randomString += String.fromCharCode(Math.floor(Math.random(100) * 100)); +/** + * Get file index data as buffer + * @returns {Buffer} + * @private + */ +const _getBufferedFileIndex = () => { + if (typeof FILE_INDEX !== 'object') { + throw new Error('FILE INDEX is not an Object'); } - return btoa(`${APP_ID}@${APP_VERSION}#${(new Date()).getTime()}-${randomString}`); + return new Buffer(JSON.stringify(FILE_INDEX)); }; -const _refreshCypherHandle = () => { - return safeCipherOpts.getHandle(ACCESS_TOKEN, - window.safeCipherOpts.getEncryptionTypes().SYMMETRIC) - .then(res => { - SYMETRIC_CYPHER_HANDLE = extractHandle(res); - return SYMETRIC_CYPHER_HANDLE; - } - ); +/** + * Prepares an Array holding different versions of a file + * @param oldData - existing file versions + * @param newData - new version of file + * @returns {Buffer} Array of file + * @private + */ +const _prepareFile = (oldData, newData) => { + if (!(oldData && Array.isArray(oldData))) { + throw new Error('oldData is not an Array'); + } + oldData.push({ + ts: (new Date()).getTime(), + content: newData + }); + return new Buffer(JSON.stringify(oldData)); }; -const _refreshConfig = () => { - // reading the config from NFS or create it if not yet existing. - const FILE_NAME = 'app_config.json'; - return safeNFS.createFile(ACCESS_TOKEN, - // try to create instead then - FILE_NAME, - JSON.stringify({ 'user_prefix': _createRandomUserPrefix() }), 'application/json') - .catch(err => err.errorCode === -505 ? '' : console.error(err)) // ignore file already exists - .then(() => safeNFS.getFile(ACCESS_TOKEN, FILE_NAME)) - .then(resp => resp.json ? resp.json() : JSON.parse(resp.toString())) - .then(config => (USER_PREFIX = config.user_prefix)); +/** + * Connect to safe network with response URI from Authenticator + * @param token + * @param resUri + * @private + */ +const _connectAuthorised = (token, resUri) => { + return window.safeApp.connectAuthorised(token, resUri) + .then((token) => (ACCESS_TOKEN = token)); }; -const getSDHandle = (filename) => { - return Promise.resolve(FILE_INDEX[filename]); +/** + * Check permission for granted access containers + * @private + */ +const _fetchAccessInfo = () => { + return window.safeApp.canAccessContainer(ACCESS_TOKEN, '_public') + .then((hasAccess) => { + if (!hasAccess) { + throw new Error('Cannot access PUBLIC Container'); + } + return true; + }); }; -const updateFile = (filename, payload) => { - let handleId = null; - return getSDHandle(filename) - .then(sdHandleId => (handleId = sdHandleId)) - .then(() => safeStructuredData.updateData(ACCESS_TOKEN, handleId, payload, SYMETRIC_CYPHER_HANDLE)) - .then(() => safeStructuredData.post(ACCESS_TOKEN, handleId)) +/** + * Creates the core mutable data (private) for the application. + * This holds `FILE_INDEX` key which act as the index for files stored. + * This mutable data has permission to - Insert, Update, Delete, ManagePermissions. + * @private + */ +const _createMdata = () => { + FILE_INDEX = {}; + return window.safeMutableData.newRandomPrivate(ACCESS_TOKEN, TYPE_TAG) + .then((mdata) => { + let permSetHandle = null; + let pubSignKeyHandle = null; + let permHandle = null; + + return window.safeMutableData.newPermissionSet(ACCESS_TOKEN) + .then((permSet) => (permSetHandle = permSet)) + .then(() => window.safeMutableDataPermissionsSet.setAllow(ACCESS_TOKEN, permSetHandle, 'Insert')) + .then(() => window.safeMutableDataPermissionsSet.setAllow(ACCESS_TOKEN, permSetHandle, 'Update')) + .then(() => window.safeMutableDataPermissionsSet.setAllow(ACCESS_TOKEN, permSetHandle, 'Delete')) + .then(() => window.safeMutableDataPermissionsSet.setAllow(ACCESS_TOKEN, permSetHandle, 'ManagePermissions')) + .then(() => window.crypto.getAppPubSignKey(ACCESS_TOKEN)) + .then((signKey) => (pubSignKeyHandle = signKey)) + .then(() => window.safeMutableData.newPermissions(ACCESS_TOKEN)) + .then((perm) => (permHandle = perm)) + .then(() => window.safeMutableDataPermissions.insertPermissionsSet(ACCESS_TOKEN, permHandle, pubSignKeyHandle, permSetHandle)) + .then(() => window.safeMutableData.newEntries(ACCESS_TOKEN)) + .then((entriesHandle) => window.safeMutableDataEntries.insert(ACCESS_TOKEN, entriesHandle, 'FILE_INDEX', _getBufferedFileIndex()) + .then(() => window.safeMutableData.put(ACCESS_TOKEN, mdata, permHandle, entriesHandle))) + .then(() => window.safeMutableData.serialise(ACCESS_TOKEN, mdata)); + }) + .then((serialisedData) => { + return window.safeApp.getContainer(ACCESS_TOKEN, '_public') + .then((mdata) => window.safeMutableData.getEntries(ACCESS_TOKEN, mdata) + .then((entries) => window.safeMutableDataEntries.mutate(ACCESS_TOKEN, entries) + .then((mut) => window.safeMutableDataMutation.insert(ACCESS_TOKEN, mut, INDEX_FILE_NAME, serialisedData) + .then(() => window.safeMutableData.applyEntriesMutation(ACCESS_TOKEN, mdata, mut))))); + }); }; -export const authorise = () => { - if (ACCESS_TOKEN) return Promise.resolve(ACCESS_TOKEN); +/** + * Read file + * @param mdata - handle of mutable data + * @param filename + * @private + * @return {data, version} - data: file data, version: file entry version. + */ +const _getFile = (mdata, filename) => { + return window.safeMutableData.emulateAs(ACCESS_TOKEN, mdata, 'NFS') + .then((nfs) => window.safeNfs.fetch(ACCESS_TOKEN, nfs, filename)) + .then((file) => { + return window.safeNfs.getFileMeta(file) + .then((meta) => window.safeImmutableData.fetch(ACCESS_TOKEN, meta.dataMapName) + .then((immut) => window.safeImmutableData.read(ACCESS_TOKEN, immut)) + .then((data) => ({ + data: JSON.parse(data.toString()), + version: meta.version + }))) + }); +}; - return safeAuth.authorise({ - 'name': APP_NAME, - 'id': APP_ID, - 'version': APP_VERSION, - 'vendor': 'MaidSafe Ltd.', - 'permissions': ['LOW_LEVEL_API'] - }, - APP_ID) - .then(res => res.__parsedResponseBody__ || res) // legacy style fallback - .then(auth => auth.token === APP_ID ? safeAuth.getAuthToken(APP_ID) : auth.token) - .then(token => { - if (!token) { - alert("Authentication failed"); - throw Error("Authentication Failed"); - } - ACCESS_TOKEN = token; - // then fetch a fresh cypherHandle - return Promise.all([ - _refreshCypherHandle(), - _refreshConfig() - ]).then(() => ACCESS_TOKEN) - } - ); +/** + * Update the file with new version + * @param filename + * @param payload + * @private + */ +const _updateFile = (filename, payload) => { + return window.safeApp.getContainer(ACCESS_TOKEN, '_public') + .then((mdata) => window.safeMutableData.getEntries(ACCESS_TOKEN, mdata)) + .then((entries) => window.safeMutableDataEntries.get(ACCESS_TOKEN, entries, INDEX_FILE_NAME)) + .then((value) => window.safeMutableData.fromSerial(ACCESS_TOKEN, value.buf)) + .then((mdata) => { + return _getFile(mdata, filename) + .then((files) => window.safeMutableData.emulateAs(ACCESS_TOKEN, mdata, 'NFS') + .then((nfs) => window.safeNfs.create(ACCESS_TOKEN, nfs, _prepareFile(files.data, payload)) + .then((file) => window.safeNfs.update(ACCESS_TOKEN, nfs, file, filename, parseInt(files.version, 10) + 1)))); + }); }; -const _putFileIndex = () => { - return safeStructuredData.updateData(ACCESS_TOKEN, - INDEX_HANDLE, - new Buffer(JSON.stringify(FILE_INDEX)).toString('base64'), - SYMETRIC_CYPHER_HANDLE) - .then(() => safeStructuredData.post(ACCESS_TOKEN, INDEX_HANDLE)); +/** + * Read file latest version + * @param filename + * @param version + */ +export const readFile = (filename, version) => { + return window.safeApp.getContainer(ACCESS_TOKEN, '_public') + .then((mdata) => window.safeMutableData.getEntries(ACCESS_TOKEN, mdata)) + .then((entries) => window.safeMutableDataEntries.get(ACCESS_TOKEN, entries, INDEX_FILE_NAME) + .then((value) => window.safeMutableData.fromSerial(ACCESS_TOKEN, value.buf) + .then((mdata) => _getFile(mdata, filename)) + .then((file) => { + return version ? file.data[version] : file.data + }))); }; +/** + * Save new file or update existing file with new version. + * @param filename + * @param data + */ export const saveFile = (filename, data) => { - const payload = new Buffer(JSON.stringify({ - ts: (new Date()).getTime(), - content: data - })).toString('base64'); - if (FILE_INDEX[filename]) { // this was an edit, add new version console.log("existing"); - return updateFile(filename, payload); + return _updateFile(filename, data); } else { - // file is being created for the first time - return safeStructuredData.create(ACCESS_TOKEN, - // trying to come up with a name that is super unlikely to clash ever. - btoa(`${USER_PREFIX}:${filename}`), - // 501 => we want this versioned - 501, payload, SYMETRIC_CYPHER_HANDLE) - .then(extractHandle) - // save the structure - .then(handle => safeStructuredData.put(ACCESS_TOKEN, handle) - // fetch a permanent reference - .then(() => safeStructuredData.getDataIdHandle(ACCESS_TOKEN, handle)) - // add the reference to the file index - .then((dataHandleId) => { - FILE_INDEX[filename] = dataHandleId; - return _putFileIndex() - }) - ) + return window.safeApp.getContainer(ACCESS_TOKEN, '_public') + .then((publicMdHandle) => window.safeMutableData.getEntries(ACCESS_TOKEN, publicMdHandle)) + .then((entHandle) => window.safeMutableDataEntries.get(ACCESS_TOKEN, entHandle, INDEX_FILE_NAME)) + .then((value) => window.safeMutableData.fromSerial(ACCESS_TOKEN, value.buf) + .then((mdata) => window.safeMutableData.emulateAs(ACCESS_TOKEN, mdata, 'NFS') + .then((nfs) => window.safeNfs.create(ACCESS_TOKEN, nfs, _prepareFile([], data)) + .then((file) => window.safeNfs.insert(ACCESS_TOKEN, nfs, file, filename))) + .then(() => window.safeMutableData.getEntries(ACCESS_TOKEN, mdata) + .then((entriesHandle) => window.safeMutableData.encryptKey(ACCESS_TOKEN, mdata, 'FILE_INDEX') + .then((key) => window.safeMutableData.get(ACCESS_TOKEN, mdata, key)) + .then((val) => window.safeMutableDataEntries.mutate(ACCESS_TOKEN, entriesHandle) + .then((mut) => { + FILE_INDEX[filename] = 1; + return window.safeMutableDataMutation.update(ACCESS_TOKEN, mut, 'FILE_INDEX', _getBufferedFileIndex(), (parseInt(val.version, 10) + 1)) + .then(() => window.safeMutableData.applyEntriesMutation(ACCESS_TOKEN, mdata, mut)); + })))))); } }; +/** + * Get all versions of a file + * @param filename + */ +export const getFileVersions = (filename) => { + return readFile(filename); +}; + +/** + * Get index of files or create the core mutable data + * @return {Promise.} + */ export const getFileIndex = () => { if (FILE_INDEX) return Promise.resolve(FILE_INDEX); - const INDEX_FILE_NAME = btoa(`${USER_PREFIX}#index`); - - return safeDataId.getStructuredDataHandle(ACCESS_TOKEN, INDEX_FILE_NAME, 500) - .then(extractHandle) - .then(handle => safeStructuredData.getHandle(ACCESS_TOKEN, handle) - .then(extractHandle) - // drop data Handle - // .then(sdHandle => safeDataId.dropHandle(handle).then(() => sdHandle)) - .then(sdHandle => { - // store the handle for future reference - INDEX_HANDLE = sdHandle; - // let's try to read - return safeStructuredData.readData(ACCESS_TOKEN, sdHandle, '') - .then(resp => resp.json ? resp.json() : JSON.parse(new Buffer(resp).toString())) - }, - (e) => { - console.error(e); - FILE_INDEX = {}; - return safeStructuredData.create(ACCESS_TOKEN, INDEX_FILE_NAME, 500, - new Buffer(JSON.stringify({})).toString('base64'), SYMETRIC_CYPHER_HANDLE) - .then(extractHandle) - .then(handle => (INDEX_HANDLE = handle)) - .then(() => safeStructuredData.put(ACCESS_TOKEN, INDEX_HANDLE) - // don't forget to clean up that handle - // .then(() => safeStructuredData.dropHandle(handle)) - ) - // and return empty data as payload - .then(() => { - return {} - }) - })) - .then(payload => { - // store payload for future reference - FILE_INDEX = payload; - return FILE_INDEX; + + return window.safeApp.getContainer(ACCESS_TOKEN, '_public') + .then((mdata) => window.safeMutableData.getEntries(ACCESS_TOKEN, mdata)) + .then((entries) => window.safeMutableDataEntries.get(ACCESS_TOKEN, entries, INDEX_FILE_NAME)) + .then((value) => window.safeMutableData.fromSerial(ACCESS_TOKEN, value.buf) + .then((mdata) => window.safeMutableData.encryptKey(ACCESS_TOKEN, mdata, 'FILE_INDEX') + .then((key) => window.safeMutableData.get(ACCESS_TOKEN, mdata, key))) + .then((fileIndex) => { + FILE_INDEX = JSON.parse(fileIndex.buf.toString()); + return FILE_INDEX; + }) + .catch(console.error) + ) + .catch(() => { + console.warn('Creating new record'); + return _createMdata(); }); }; -export const readFile = (filename, version) => { - return getSDHandle(filename) - .then(handleId => safeStructuredData.readData(ACCESS_TOKEN, handleId, version)); -}; +/** + * Authorise with Authenticator. + * Already authorised get the response URI from local storage and connect with SAFE Network. + * @return {Promise.} + */ +export const authorise = () => { + if (ACCESS_TOKEN) return Promise.resolve(ACCESS_TOKEN); + + const responseUri = _getResponseUri(); -export const getSDVersions = (filename) => { - let sdHandleId = null; - return getSDHandle(filename) - .then(handleId => (sdHandleId = handleId)) - .then(() => safeStructuredData.getMetadata(ACCESS_TOKEN, sdHandleId)) - .then(res => res.hasOwnProperty('version') ? res.version : res.__parsedResponseBody__.version) - .then(sdVersion => { - const iterator = []; - for (let i = 0; i <= sdVersion; i++) { - iterator.push(i); + return window.safeApp.initialise(APP_INFO) + .then((token) => { + if (responseUri) { + return _connectAuthorised(token, responseUri); } - return Promise.all(iterator.map(version => safeStructuredData.readData(ACCESS_TOKEN, sdHandleId, version))); - }); + return window.safeApp.authorise(token, CONTAINERS) + .then((resUri) => { + _saveResponseUri(resUri); + return _connectAuthorised(token, resUri); + }); + }) + .then(() => _fetchAccessInfo()); }; From 9d48caea85e3ded1bad9c3fe2a0d86363092c10e Mon Sep 17 00:00:00 2001 From: Shankar Date: Wed, 3 May 2017 13:44:57 +0530 Subject: [PATCH 15/44] fix/create_public_id: update to latest safe_app_nodejs (#171) Updated to latest safe_app_nodejs crypto API. --- web_hosting_manager/app/lib/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_hosting_manager/app/lib/api.js b/web_hosting_manager/app/lib/api.js index 7f4504c..5a0aac7 100644 --- a/web_hosting_manager/app/lib/api.js +++ b/web_hosting_manager/app/lib/api.js @@ -174,7 +174,7 @@ export const createPublicId = (publicId) => { .then(() => permissionSet.setAllow('Update')) .then(() => permissionSet.setAllow('Delete')) .then(() => permissionSet.setAllow('ManagePermissions')) - .then(() => safe.auth.getPubSignKey()) + .then(() => safe.crypto.getAppPubSignKey()) .then((signKey) => (pubSignKey = signKey)) .then(() => safe.mutableData.newPermissions()) .then((perm) => (permissions = perm)) From a1d28e75e5e8b9325495afe557ed92529f61b6ed Mon Sep 17 00:00:00 2001 From: Shankar Date: Wed, 3 May 2017 16:50:05 +0530 Subject: [PATCH 16/44] fix/crypto_sha: update to use node crypto sha to safe crypto sha (#173) Update to use safe crypto sha for public name. --- web_hosting_manager/app/lib/api.js | 11 ++++++----- web_hosting_manager/app/lib/utils.js | 4 ---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/web_hosting_manager/app/lib/api.js b/web_hosting_manager/app/lib/api.js index 5a0aac7..cb6b07c 100644 --- a/web_hosting_manager/app/lib/api.js +++ b/web_hosting_manager/app/lib/api.js @@ -6,7 +6,7 @@ import Downloader from './Downloader'; import { I18n } from 'react-redux-i18n'; import safeApp from 'safe-app'; import pkg from '../package.json'; -import { hashString, strToPtrBuf, parseUrl } from './utils'; +import { parseUrl } from './utils'; const SERVICE = 'WEB_HOST_MANAGER'; const ACCOUNT = 'SAFE_USER'; @@ -43,7 +43,7 @@ export const accessContainers = { publicNames: '_publicNames' }; -export const typetag = 1500; +export const typetag = 15001; let publicIds = {}; let uploader; @@ -159,10 +159,10 @@ export const createPublicId = (publicId) => { const err = new Error(I18n.t('messages.cannotBeEmpty', { name: 'Public Id' })); return Promise.reject(err); } - let hashedPubId = hashString(publicId); let publicIdName = null; - return safe.mutableData.newPublic(hashedPubId, typetag) + return safe.crypto.sha3Hash(publicId) + .then((hashVal) => safe.mutableData.newPublic(hashVal, typetag)) .then((mdata) => { let permissionSet = null; let permissions = null; @@ -213,7 +213,8 @@ export const createService = (publicId, service, container) => { }; export const deleteService = (publicId, service) => { - return safe.mutableData.newPublic(hashString(publicId), typetag) + return safe.crypto.sha3Hash(publicId) + .then((hashVal) => safe.mutableData.newPublic(hashVal, typetag)) .then((mdata) => mdata.getEntries() .then((entries) => entries.get(service) .then((val) => entries.mutate() diff --git a/web_hosting_manager/app/lib/utils.js b/web_hosting_manager/app/lib/utils.js index fead7b4..1c9fb44 100644 --- a/web_hosting_manager/app/lib/utils.js +++ b/web_hosting_manager/app/lib/utils.js @@ -99,10 +99,6 @@ export const generateUploadTaskQueue = (localPath, networkPath, callback) => { return taskQueue; }; -export const hashString = (str) => ( - crypto.createHash('sha256').update(str).digest() -); - export const strToPtrBuf = (str) => { const buf = new Buffer(str); return { ptr: buf, len: buf.length }; From cff45c2a472df4184128ba809b2ce781288e76c6 Mon Sep 17 00:00:00 2001 From: Shankar Date: Wed, 3 May 2017 17:44:06 +0530 Subject: [PATCH 17/44] fix/typo: update crypto to safeCrypto (#172) Updated crypto to safeCrypto --- markdown_editor/src/store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/markdown_editor/src/store.js b/markdown_editor/src/store.js index 7fc8150..3d6022e 100644 --- a/markdown_editor/src/store.js +++ b/markdown_editor/src/store.js @@ -121,7 +121,7 @@ const _createMdata = () => { .then(() => window.safeMutableDataPermissionsSet.setAllow(ACCESS_TOKEN, permSetHandle, 'Update')) .then(() => window.safeMutableDataPermissionsSet.setAllow(ACCESS_TOKEN, permSetHandle, 'Delete')) .then(() => window.safeMutableDataPermissionsSet.setAllow(ACCESS_TOKEN, permSetHandle, 'ManagePermissions')) - .then(() => window.crypto.getAppPubSignKey(ACCESS_TOKEN)) + .then(() => window.safeCrypto.getAppPubSignKey(ACCESS_TOKEN)) .then((signKey) => (pubSignKeyHandle = signKey)) .then(() => window.safeMutableData.newPermissions(ACCESS_TOKEN)) .then((perm) => (permHandle = perm)) From 381585cafcb0f96ad79cd2f26416961a9403d422 Mon Sep 17 00:00:00 2001 From: Shankar Date: Thu, 4 May 2017 12:31:06 +0530 Subject: [PATCH 18/44] feat/use_home_container: update to use home container (#175) Updated to use home container to store datas instead of using _public container --- markdown_editor/src/config.js | 5 +- markdown_editor/src/store.js | 132 ++++++++++++---------------------- 2 files changed, 51 insertions(+), 86 deletions(-) diff --git a/markdown_editor/src/config.js b/markdown_editor/src/config.js index ef3d8bc..d98077d 100644 --- a/markdown_editor/src/config.js +++ b/markdown_editor/src/config.js @@ -10,7 +10,6 @@ export const EDITOR_THEME = 'mdn-like'; export const APP_NAME = "SAFE Markdown Editor"; export const APP_VERSION = pkg.version; export const APP_ID = 'net.maidsafe.examples.markdown-editor'; -export const TYPE_TAG = 15463; export const APP_INFO = { id: APP_ID, name: APP_NAME, @@ -18,6 +17,10 @@ export const APP_INFO = { scope: '' }; +export const APP_INFO_OPTS = { + own_container: true +}; + export const CONTAINERS = { '_public': ['Read', 'Insert', 'Update', 'Delete', 'ManagePermissions'] }; diff --git a/markdown_editor/src/store.js b/markdown_editor/src/store.js index 3d6022e..4cfda53 100644 --- a/markdown_editor/src/store.js +++ b/markdown_editor/src/store.js @@ -1,6 +1,5 @@ /* global btoa, safeAuth, safeNFS safeCipherOpts, safeStructuredData, safeDataId */ -import crypto from 'crypto'; -import { APP_ID, APP_INFO, CONTAINERS, TYPE_TAG } from './config.js'; +import { APP_INFO, APP_INFO_OPTS, CONTAINERS } from './config.js'; const requiredWindowObj = [ 'safeApp', @@ -19,12 +18,13 @@ requiredWindowObj.forEach((obj) => { } }); -const INDEX_FILE_NAME = crypto.createHash('sha256').update(`${window.location.host}-${APP_ID}`).digest('hex'); const RES_URI_KEY = 'SAFE_RES_URI'; +const FILE_INDEX_KEY = 'FILE_INDEX'; // global access state let ACCESS_TOKEN; let FILE_INDEX; +let HOME_CONTAINER_HANDLE; /** * Save response URI to local storage @@ -76,6 +76,15 @@ const _prepareFile = (oldData, newData) => { return new Buffer(JSON.stringify(oldData)); }; +const _getHomeContainer = () => { + if (HOME_CONTAINER_HANDLE) { + return Promise.resolve(HOME_CONTAINER_HANDLE); + } + return window.safeApp.getHomeContainer(ACCESS_TOKEN) + .then((mdata) => (HOME_CONTAINER_HANDLE = mdata)) + .then(() => HOME_CONTAINER_HANDLE); +}; + /** * Connect to safe network with response URI from Authenticator * @param token @@ -101,45 +110,6 @@ const _fetchAccessInfo = () => { }); }; -/** - * Creates the core mutable data (private) for the application. - * This holds `FILE_INDEX` key which act as the index for files stored. - * This mutable data has permission to - Insert, Update, Delete, ManagePermissions. - * @private - */ -const _createMdata = () => { - FILE_INDEX = {}; - return window.safeMutableData.newRandomPrivate(ACCESS_TOKEN, TYPE_TAG) - .then((mdata) => { - let permSetHandle = null; - let pubSignKeyHandle = null; - let permHandle = null; - - return window.safeMutableData.newPermissionSet(ACCESS_TOKEN) - .then((permSet) => (permSetHandle = permSet)) - .then(() => window.safeMutableDataPermissionsSet.setAllow(ACCESS_TOKEN, permSetHandle, 'Insert')) - .then(() => window.safeMutableDataPermissionsSet.setAllow(ACCESS_TOKEN, permSetHandle, 'Update')) - .then(() => window.safeMutableDataPermissionsSet.setAllow(ACCESS_TOKEN, permSetHandle, 'Delete')) - .then(() => window.safeMutableDataPermissionsSet.setAllow(ACCESS_TOKEN, permSetHandle, 'ManagePermissions')) - .then(() => window.safeCrypto.getAppPubSignKey(ACCESS_TOKEN)) - .then((signKey) => (pubSignKeyHandle = signKey)) - .then(() => window.safeMutableData.newPermissions(ACCESS_TOKEN)) - .then((perm) => (permHandle = perm)) - .then(() => window.safeMutableDataPermissions.insertPermissionsSet(ACCESS_TOKEN, permHandle, pubSignKeyHandle, permSetHandle)) - .then(() => window.safeMutableData.newEntries(ACCESS_TOKEN)) - .then((entriesHandle) => window.safeMutableDataEntries.insert(ACCESS_TOKEN, entriesHandle, 'FILE_INDEX', _getBufferedFileIndex()) - .then(() => window.safeMutableData.put(ACCESS_TOKEN, mdata, permHandle, entriesHandle))) - .then(() => window.safeMutableData.serialise(ACCESS_TOKEN, mdata)); - }) - .then((serialisedData) => { - return window.safeApp.getContainer(ACCESS_TOKEN, '_public') - .then((mdata) => window.safeMutableData.getEntries(ACCESS_TOKEN, mdata) - .then((entries) => window.safeMutableDataEntries.mutate(ACCESS_TOKEN, entries) - .then((mut) => window.safeMutableDataMutation.insert(ACCESS_TOKEN, mut, INDEX_FILE_NAME, serialisedData) - .then(() => window.safeMutableData.applyEntriesMutation(ACCESS_TOKEN, mdata, mut))))); - }); -}; - /** * Read file * @param mdata - handle of mutable data @@ -168,10 +138,7 @@ const _getFile = (mdata, filename) => { * @private */ const _updateFile = (filename, payload) => { - return window.safeApp.getContainer(ACCESS_TOKEN, '_public') - .then((mdata) => window.safeMutableData.getEntries(ACCESS_TOKEN, mdata)) - .then((entries) => window.safeMutableDataEntries.get(ACCESS_TOKEN, entries, INDEX_FILE_NAME)) - .then((value) => window.safeMutableData.fromSerial(ACCESS_TOKEN, value.buf)) + return _getHomeContainer() .then((mdata) => { return _getFile(mdata, filename) .then((files) => window.safeMutableData.emulateAs(ACCESS_TOKEN, mdata, 'NFS') @@ -186,14 +153,11 @@ const _updateFile = (filename, payload) => { * @param version */ export const readFile = (filename, version) => { - return window.safeApp.getContainer(ACCESS_TOKEN, '_public') - .then((mdata) => window.safeMutableData.getEntries(ACCESS_TOKEN, mdata)) - .then((entries) => window.safeMutableDataEntries.get(ACCESS_TOKEN, entries, INDEX_FILE_NAME) - .then((value) => window.safeMutableData.fromSerial(ACCESS_TOKEN, value.buf) - .then((mdata) => _getFile(mdata, filename)) - .then((file) => { - return version ? file.data[version] : file.data - }))); + return _getHomeContainer() + .then((mdata) => _getFile(mdata, filename)) + .then((file) => { + return version ? file.data[version] : file.data + }); }; /** @@ -207,22 +171,19 @@ export const saveFile = (filename, data) => { console.log("existing"); return _updateFile(filename, data); } else { - return window.safeApp.getContainer(ACCESS_TOKEN, '_public') - .then((publicMdHandle) => window.safeMutableData.getEntries(ACCESS_TOKEN, publicMdHandle)) - .then((entHandle) => window.safeMutableDataEntries.get(ACCESS_TOKEN, entHandle, INDEX_FILE_NAME)) - .then((value) => window.safeMutableData.fromSerial(ACCESS_TOKEN, value.buf) - .then((mdata) => window.safeMutableData.emulateAs(ACCESS_TOKEN, mdata, 'NFS') - .then((nfs) => window.safeNfs.create(ACCESS_TOKEN, nfs, _prepareFile([], data)) - .then((file) => window.safeNfs.insert(ACCESS_TOKEN, nfs, file, filename))) - .then(() => window.safeMutableData.getEntries(ACCESS_TOKEN, mdata) - .then((entriesHandle) => window.safeMutableData.encryptKey(ACCESS_TOKEN, mdata, 'FILE_INDEX') - .then((key) => window.safeMutableData.get(ACCESS_TOKEN, mdata, key)) - .then((val) => window.safeMutableDataEntries.mutate(ACCESS_TOKEN, entriesHandle) - .then((mut) => { - FILE_INDEX[filename] = 1; - return window.safeMutableDataMutation.update(ACCESS_TOKEN, mut, 'FILE_INDEX', _getBufferedFileIndex(), (parseInt(val.version, 10) + 1)) - .then(() => window.safeMutableData.applyEntriesMutation(ACCESS_TOKEN, mdata, mut)); - })))))); + return _getHomeContainer() + .then((mdata) => window.safeMutableData.emulateAs(ACCESS_TOKEN, mdata, 'NFS') + .then((nfs) => window.safeNfs.create(ACCESS_TOKEN, nfs, _prepareFile([], data)) + .then((file) => window.safeNfs.insert(ACCESS_TOKEN, nfs, file, filename))) + .then(() => window.safeMutableData.getEntries(ACCESS_TOKEN, mdata) + .then((entriesHandle) => window.safeMutableData.encryptKey(ACCESS_TOKEN, mdata, FILE_INDEX_KEY) + .then((key) => window.safeMutableData.get(ACCESS_TOKEN, mdata, key)) + .then((val) => window.safeMutableDataEntries.mutate(ACCESS_TOKEN, entriesHandle) + .then((mut) => { + FILE_INDEX[filename] = 1; + return window.safeMutableDataMutation.update(ACCESS_TOKEN, mut, FILE_INDEX_KEY, _getBufferedFileIndex(), (parseInt(val.version, 10) + 1)) + .then(() => window.safeMutableData.applyEntriesMutation(ACCESS_TOKEN, mdata, mut)); + }))))); } }; @@ -235,28 +196,29 @@ export const getFileVersions = (filename) => { }; /** - * Get index of files or create the core mutable data - * @return {Promise.} + * Get index of files or prepare home container + * @return {Promise} */ export const getFileIndex = () => { if (FILE_INDEX) return Promise.resolve(FILE_INDEX); - return window.safeApp.getContainer(ACCESS_TOKEN, '_public') - .then((mdata) => window.safeMutableData.getEntries(ACCESS_TOKEN, mdata)) - .then((entries) => window.safeMutableDataEntries.get(ACCESS_TOKEN, entries, INDEX_FILE_NAME)) - .then((value) => window.safeMutableData.fromSerial(ACCESS_TOKEN, value.buf) - .then((mdata) => window.safeMutableData.encryptKey(ACCESS_TOKEN, mdata, 'FILE_INDEX') - .then((key) => window.safeMutableData.get(ACCESS_TOKEN, mdata, key))) + return _getHomeContainer() + .then((mdata) => window.safeMutableData.encryptKey(ACCESS_TOKEN, mdata, FILE_INDEX_KEY) + .then((key) => window.safeMutableData.get(ACCESS_TOKEN, mdata, key)) .then((fileIndex) => { FILE_INDEX = JSON.parse(fileIndex.buf.toString()); return FILE_INDEX; }) - .catch(console.error) - ) - .catch(() => { - console.warn('Creating new record'); - return _createMdata(); - }); + .catch(() => { + FILE_INDEX = {}; + console.warn('Preparing Home container'); + + // FIXME: check for exact error condition. + return window.safeMutableData.getEntries(ACCESS_TOKEN, mdata) + .then((entriesHandle) => window.safeMutableDataEntries.mutate(ACCESS_TOKEN, entriesHandle)) + .then((mut) => window.safeMutableDataMutation.insert(ACCESS_TOKEN, mut, FILE_INDEX_KEY, _getBufferedFileIndex()) + .then(() => window.safeMutableData.applyEntriesMutation(ACCESS_TOKEN, mdata, mut))); + })); }; /** @@ -274,7 +236,7 @@ export const authorise = () => { if (responseUri) { return _connectAuthorised(token, responseUri); } - return window.safeApp.authorise(token, CONTAINERS) + return window.safeApp.authorise(token, CONTAINERS, APP_INFO_OPTS) .then((resUri) => { _saveResponseUri(resUri); return _connectAuthorised(token, resUri); From 01a11fb7533da31cb3516fafe3206db104985755 Mon Sep 17 00:00:00 2001 From: Gabriel Viganotti Date: Thu, 4 May 2017 14:53:45 -0300 Subject: [PATCH 19/44] fix/Authorisation was not working on windows due to a known bug in safe_app_nodejs. (#174) fix/README was updated. fix/email addresses are now being returned as array so they are now serialised to store them in the inbox. --- email_app/README.md | 14 +++++---- email_app/app/index.js | 2 +- email_app/app/safenet_comm.js | 49 ++++++++++++++++++-------------- email_app/app/utils/app_utils.js | 9 ++++++ 4 files changed, 45 insertions(+), 29 deletions(-) diff --git a/email_app/README.md b/email_app/README.md index b634bb5..f44bbe2 100644 --- a/email_app/README.md +++ b/email_app/README.md @@ -1,14 +1,16 @@ # Safe Mail Tutorial Application -The tutorial app show cases how to use the low level API from launcher to -build a simple email application. +The tutorial app show cases how to use the low level API from `safe_app_nodejs` +library to build a simple email application. -Demonstrates the usage of - - Private AppendableData - - StructuredData +Demonstrates the usage of: + - Private MutableData + - Public MutableData - Immutable data + - App's home container + - `_publicNames` and services containers -Requires [safe_launcher](https://github.com/maidsafe/safe_launcher) version 0.9.1 +Please refer to the [Application Data Model](#application-data-model) section below for additional details. ## Install diff --git a/email_app/app/index.js b/email_app/app/index.js index 591a0af..18a2379 100755 --- a/email_app/app/index.js +++ b/email_app/app/index.js @@ -21,7 +21,7 @@ const createWindow = () => { mainWindow.loadURL(`file://${__dirname}/app.html`); // Open the DevTools. - mainWindow.webContents.openDevTools(); + // mainWindow.webContents.openDevTools(); // Emitted when the window is closed. mainWindow.on('closed', () => { diff --git a/email_app/app/safenet_comm.js b/email_app/app/safenet_comm.js index 7554c7d..7b00953 100644 --- a/email_app/app/safenet_comm.js +++ b/email_app/app/safenet_comm.js @@ -1,7 +1,8 @@ +import { shell } from 'electron'; import { CONSTANTS, MESSAGES } from './constants'; import { initializeApp, fromAuthURI } from 'safe-app'; import { getAuthData, saveAuthData, clearAuthData, hashPublicId, genRandomEntryKey, - genKeyPair, encrypt, decrypt, genServiceInfo } from './utils/app_utils'; + genKeyPair, encrypt, decrypt, genServiceInfo, deserialiseArray, parseUrl } from './utils/app_utils'; import pkg from '../package.json'; const APP_INFO = { @@ -14,6 +15,9 @@ const APP_INFO = { opts: { own_container: true }, + containers: { + publicNames: '_publicNames' + }, permissions: { _publicNames: ['Read', 'Insert', 'Update'] } @@ -22,7 +26,10 @@ const APP_INFO = { const requestAuth = () => { return initializeApp(APP_INFO.info) .then((app) => app.auth.genAuthUri(APP_INFO.permissions, APP_INFO.opts) - .then((resp) => app.auth.openUri(resp.uri)) + .then((resp) => { + shell.openExternal(parseUrl(resp.uri)); + return null; + }) ); } @@ -37,29 +44,29 @@ export const authApp = () => { return fromAuthURI(APP_INFO.info, uri) .then((registered_app) => registered_app.auth.refreshContainerAccess() .then(() => registered_app) - .catch((err) => { - console.warn("Auth URI stored is not valid anymore, app needs to be re-authorised."); - clearAuthData(); - return requestAuth(); - }) - ); + ) + .catch((err) => { + console.warn("Auth URI stored is not valid anymore, app needs to be re-authorised."); + clearAuthData(); + return requestAuth(); + }); } return requestAuth(); } export const connect = (uri) => { + let registered_app; return fromAuthURI(APP_INFO.info, uri) - .then((app) => { - saveAuthData(uri); - return app; - }); + .then((app) => registered_app = app) + .then(() => saveAuthData(uri)) + .then(() => registered_app.auth.refreshContainerAccess()) + .then(() => registered_app); } export const readConfig = (app) => { let account = {}; - return app.auth.refreshContainerAccess() - .then(() => app.auth.getHomeContainer()) + return app.auth.getHomeContainer() .then((md) => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_INBOX).then((key) => md.get(key)) .then((value) => app.mutableData.fromSerial(value.buf)) .then((inbox_md) => account.inbox_md = inbox_md) @@ -83,7 +90,6 @@ export const writeConfig = (app, account) => { .then((serial) => serialised_inbox = serial) .then(() => account.archive_md.serialise()) .then((serial) => serialised_archive = serial) - .then(() => app.auth.refreshContainerAccess()) .then(() => app.auth.getHomeContainer()) .then((md) => app.mutableData.newMutation() .then((mut) => mut.insert(CONSTANTS.MD_KEY_EMAIL_INBOX, serialised_inbox) @@ -102,7 +108,7 @@ export const readInboxEmails = (app, account, cb) => { if (key.toString() !== CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY && value.buf.toString().length > 0) { //FIXME: this condition is a work around for a limitation in safe_core let entry_value = decrypt(value.buf.toString(), account.enc_sk, account.enc_pk); - return app.immutableData.fetch(Buffer.from(entry_value, 'hex')) + return app.immutableData.fetch(deserialiseArray(entry_value)) .then((immData) => immData.read()) .then((content) => { let decryptedEmail; @@ -117,8 +123,9 @@ export const readInboxEmails = (app, account, cb) => { export const readArchivedEmails = (app, account, cb) => { return account.archive_md.getEntries() .then((entries) => entries.forEach((key, value) => { - if (value.buf.toString().length > 0) { //FIXME: this condition is a work around for a limitation in safe_core - return app.immutableData.fetch(value.buf) + let emailAddr = value.buf.toString(); + if (emailAddr.length > 0) { //FIXME: this condition is a work around for a limitation in safe_core + return app.immutableData.fetch(deserialiseArray(emailAddr)) .then((immData) => immData.read()) .then((content) => { let decryptedEmail; @@ -181,8 +188,7 @@ export const setupAccount = (app, emailId) => { enc_sk: key_pair.privateKey, enc_pk: key_pair.publicKey}) .then(() => newAccount.inbox_md.serialise()) .then((md_serialised) => inbox_serialised = md_serialised) - .then(() => app.auth.refreshContainerAccess()) - .then(() => app.auth.getAccessContainerInfo('_publicNames')) + .then(() => app.auth.getAccessContainerInfo(APP_INFO.containers.publicNames)) .then((pub_names_md) => pub_names_md.encryptKey(serviceInfo.publicId) .then((encrypted_publicId) => pub_names_md.get(encrypted_publicId)) .then((services) => addService(app, serviceInfo, inbox_serialised) @@ -216,8 +222,8 @@ export const storeEmail = (app, email, to) => { .then((pk) => writeEmailContent(app, email, pk.buf.toString()) .then((email_addr) => app.mutableData.newMutation() .then((mut) => { - let entry_value = encrypt(email_addr.buffer.toString('hex'), pk.buf.toString()); let entry_key = genRandomEntryKey(); + let entry_value = encrypt(email_addr.toString(), pk.buf.toString()); return mut.insert(entry_key, entry_value) .then(() => inbox_md.applyEntriesMutation(mut)) }) @@ -243,7 +249,6 @@ export const archiveEmail = (app, account, key) => { let new_entry_key = genRandomEntryKey(); return account.inbox_md.get(key) .then((encryptedEmailXorName) => decrypt(encryptedEmailXorName.buf.toString(), account.enc_sk, account.enc_pk)) - .then((decrytedXorNameHex) => Buffer.from(decrytedXorNameHex, 'hex')) .then((xorName) => app.mutableData.newMutation() .then((mut) => mut.insert(new_entry_key, xorName) .then(() => account.archive_md.applyEntriesMutation(mut)) diff --git a/email_app/app/utils/app_utils.js b/email_app/app/utils/app_utils.js index 7722cee..fc424d9 100644 --- a/email_app/app/utils/app_utils.js +++ b/email_app/app/utils/app_utils.js @@ -59,6 +59,15 @@ export const showSuccess = (title, message) => { }, _ => {}); }; +export const parseUrl = (url) => ( + (url.indexOf('safe-auth://') === -1) ? url.replace('safe-auth:', 'safe-auth://') : url +); + +export const deserialiseArray = (str) => { + let arrItems = str.split(','); + return Uint8Array.from(arrItems); +} + export const genKeyPair = () => { let {keyType, privateKey, publicKey} = sodium.crypto_box_keypair('hex'); return {privateKey, publicKey}; From c5f76e5d32afb03446dea66b8c0d4b1a68d36463 Mon Sep 17 00:00:00 2001 From: Gabriel Viganotti Date: Wed, 17 May 2017 15:07:22 +0200 Subject: [PATCH 20/44] feat/adapt code to encrypt and decrypt entries for private MDs and HomeContainer due to changes in safe_app lib (#177) --- email_app/app/actions/mail_actions.js | 18 +---- email_app/app/components/mail_list.js | 8 +- .../app/containers/mail_inbox_container.js | 4 +- .../app/containers/mail_saved_container.js | 4 +- email_app/app/safenet_comm.js | 78 +++++++++---------- 5 files changed, 47 insertions(+), 65 deletions(-) diff --git a/email_app/app/actions/mail_actions.js b/email_app/app/actions/mail_actions.js index 3ae1873..13fec7f 100644 --- a/email_app/app/actions/mail_actions.js +++ b/email_app/app/actions/mail_actions.js @@ -1,5 +1,5 @@ import ACTION_TYPES from './actionTypes'; -import { storeEmail, removeInboxEmail, removeArchivedEmail, archiveEmail } from '../safenet_comm'; +import { storeEmail, removeEmail, archiveEmail } from '../safenet_comm'; export const sendEmail = (email, to) => { return function (dispatch, getState) { @@ -25,24 +25,12 @@ export const saveEmail = (account, key) => { }; }; -export const deleteInboxEmail = (account, key) => { +export const deleteEmail = (container, key) => { return function (dispatch, getState) { let app = getState().initializer.app; return dispatch({ type: ACTION_TYPES.MAIL_PROCESSING, - payload: removeInboxEmail(app, account, key) - .then(() => dispatch(clearMailProcessing)) - .then(() => Promise.resolve()) - }); - }; -}; - -export const deleteSavedEmail = (account, key) => { - return function (dispatch, getState) { - let app = getState().initializer.app; - return dispatch({ - type: ACTION_TYPES.MAIL_PROCESSING, - payload: removeArchivedEmail(app, account, key) + payload: removeEmail(app, container, key) .then(() => dispatch(clearMailProcessing)) .then(() => Promise.resolve()) }); diff --git a/email_app/app/components/mail_list.js b/email_app/app/components/mail_list.js index 9b78ae1..c680375 100644 --- a/email_app/app/components/mail_list.js +++ b/email_app/app/components/mail_list.js @@ -40,8 +40,8 @@ export default class MailList extends Component { handleDeleteFromInbox(e) { e.preventDefault(); - const { accounts, deleteInboxEmail, refreshEmail } = this.props; - deleteInboxEmail(accounts, e.target.dataset.index) + const { accounts, deleteEmail, refreshEmail } = this.props; + deleteEmail(accounts.inbox_md, e.target.dataset.index) .catch((error) => { console.error('Failed trying to delete email from inbox: ', error); showError('Failed trying to delete email from inbox: ', error); @@ -51,8 +51,8 @@ export default class MailList extends Component { handleDeleteSaved(e) { e.preventDefault(); - const { accounts, deleteSavedEmail, refreshEmail } = this.props; - deleteSavedEmail(accounts, e.target.dataset.index) + const { accounts, deleteEmail, refreshEmail } = this.props; + deleteEmail(accounts.archive_md, e.target.dataset.index) .catch((error) => { console.error('Failed trying to delete saved email: ', error); showError('Failed trying to delete saved email: ', error); diff --git a/email_app/app/containers/mail_inbox_container.js b/email_app/app/containers/mail_inbox_container.js index 0eed22c..752ae40 100644 --- a/email_app/app/containers/mail_inbox_container.js +++ b/email_app/app/containers/mail_inbox_container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import MailInbox from '../components/mail_inbox'; -import { refreshInbox, clearMailProcessing, deleteInboxEmail, saveEmail } from '../actions/mail_actions'; +import { refreshInbox, clearMailProcessing, deleteEmail, saveEmail } from '../actions/mail_actions'; import { refreshEmail } from '../actions/initializer_actions'; const mapStateToProps = state => { @@ -18,7 +18,7 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { refreshEmail: (account) => (dispatch(refreshEmail(account))), - deleteInboxEmail: (account, key) => (dispatch(deleteInboxEmail(account, key))), + deleteEmail: (account, key) => (dispatch(deleteEmail(account, key))), clearMailProcessing: () => (dispatch(clearMailProcessing())), saveEmail: (account, key) => (dispatch(saveEmail(account, key))) }; diff --git a/email_app/app/containers/mail_saved_container.js b/email_app/app/containers/mail_saved_container.js index a45282a..973a4c9 100644 --- a/email_app/app/containers/mail_saved_container.js +++ b/email_app/app/containers/mail_saved_container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import MailSaved from '../components/mail_saved'; -import { deleteSavedEmail } from '../actions/mail_actions'; +import { deleteEmail } from '../actions/mail_actions'; import { refreshEmail } from '../actions/initializer_actions'; const mapStateToProps = state => { @@ -16,7 +16,7 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { refreshEmail: (account) => (dispatch(refreshEmail(account))), - deleteSavedEmail: (account, key) => (dispatch(deleteSavedEmail(account, key))) + deleteEmail: (account, key) => (dispatch(deleteEmail(account, key))) }; }; diff --git a/email_app/app/safenet_comm.js b/email_app/app/safenet_comm.js index 7b00953..05d1789 100644 --- a/email_app/app/safenet_comm.js +++ b/email_app/app/safenet_comm.js @@ -68,21 +68,28 @@ export const readConfig = (app) => { let account = {}; return app.auth.getHomeContainer() .then((md) => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_INBOX).then((key) => md.get(key)) - .then((value) => app.mutableData.fromSerial(value.buf)) + .then((value) => md.decrypt(value.buf).then((decrypted) => app.mutableData.fromSerial(decrypted))) .then((inbox_md) => account.inbox_md = inbox_md) .then(() => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_ARCHIVE).then((key) => md.get(key))) - .then((value) => app.mutableData.fromSerial(value.buf)) + .then((value) => md.decrypt(value.buf).then((decrypted) => app.mutableData.fromSerial(decrypted))) .then((archive_md) => account.archive_md = archive_md) .then(() => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_ID).then((key) => md.get(key))) - .then((value) => account.id = value.buf.toString()) + .then((value) => md.decrypt(value.buf).then((decrypted) => account.id = decrypted.toString())) .then(() => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_ENC_SECRET_KEY).then((key) => md.get(key))) - .then((value) => account.enc_sk = value.buf.toString()) + .then((value) => md.decrypt(value.buf).then((decrypted) => account.enc_sk = decrypted.toString())) .then(() => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY).then((key) => md.get(key))) - .then((value) => account.enc_pk = value.buf.toString()) + .then((value) => md.decrypt(value.buf).then((decrypted) => account.enc_pk = decrypted.toString())) ) .then(() => account); } +const insertEncrypted = (md, mut, key, value) => { + return md.encryptKey(key) + .then((encrypted_key) => md.encryptValue(value) + .then((encrypted_value) => mut.insert(encrypted_key, encrypted_value)) + ); +} + export const writeConfig = (app, account) => { let serialised_inbox; let serialised_archive; @@ -92,29 +99,34 @@ export const writeConfig = (app, account) => { .then((serial) => serialised_archive = serial) .then(() => app.auth.getHomeContainer()) .then((md) => app.mutableData.newMutation() - .then((mut) => mut.insert(CONSTANTS.MD_KEY_EMAIL_INBOX, serialised_inbox) - .then(() => mut.insert(CONSTANTS.MD_KEY_EMAIL_ARCHIVE, serialised_archive)) - .then(() => mut.insert(CONSTANTS.MD_KEY_EMAIL_ID, account.id)) - .then(() => mut.insert(CONSTANTS.MD_KEY_EMAIL_ENC_SECRET_KEY, account.enc_sk)) - .then(() => mut.insert(CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY, account.enc_pk)) + .then((mut) => insertEncrypted(md, mut, CONSTANTS.MD_KEY_EMAIL_INBOX, serialised_inbox) + .then(() => insertEncrypted(md, mut, CONSTANTS.MD_KEY_EMAIL_ARCHIVE, serialised_archive)) + .then(() => insertEncrypted(md, mut, CONSTANTS.MD_KEY_EMAIL_ID, account.id)) + .then(() => insertEncrypted(md, mut, CONSTANTS.MD_KEY_EMAIL_ENC_SECRET_KEY, account.enc_sk)) + .then(() => insertEncrypted(md, mut, CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY, account.enc_pk)) .then(() => md.applyEntriesMutation(mut)) )) .then(() => account); } +const decryptEmail = (app, account, key, value, cb) => { + if (value.length > 0) { //FIXME: this condition is a work around for a limitation in safe_core + let entry_value = decrypt(value, account.enc_sk, account.enc_pk); + return app.immutableData.fetch(deserialiseArray(entry_value)) + .then((immData) => immData.read()) + .then((content) => { + let decryptedEmail; + decryptedEmail = JSON.parse(decrypt(content.toString(), account.enc_sk, account.enc_pk)); + cb({ [key]: decryptedEmail }); + }); + } +} + export const readInboxEmails = (app, account, cb) => { return account.inbox_md.getEntries() .then((entries) => entries.forEach((key, value) => { - if (key.toString() !== CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY - && value.buf.toString().length > 0) { //FIXME: this condition is a work around for a limitation in safe_core - let entry_value = decrypt(value.buf.toString(), account.enc_sk, account.enc_pk); - return app.immutableData.fetch(deserialiseArray(entry_value)) - .then((immData) => immData.read()) - .then((content) => { - let decryptedEmail; - decryptedEmail = JSON.parse(decrypt(content.toString(), account.enc_sk, account.enc_pk)); - cb({ [key]: decryptedEmail }); - }) + if (key.toString() !== CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY) { + return decryptEmail(app, account, key, value.buf.toString(), cb); } }) ); @@ -123,16 +135,7 @@ export const readInboxEmails = (app, account, cb) => { export const readArchivedEmails = (app, account, cb) => { return account.archive_md.getEntries() .then((entries) => entries.forEach((key, value) => { - let emailAddr = value.buf.toString(); - if (emailAddr.length > 0) { //FIXME: this condition is a work around for a limitation in safe_core - return app.immutableData.fetch(deserialiseArray(emailAddr)) - .then((immData) => immData.read()) - .then((content) => { - let decryptedEmail; - decryptedEmail = JSON.parse(decrypt(content.toString(), account.enc_sk, account.enc_pk)); - cb({ [key]: decryptedEmail }); - }) - } + return decryptEmail(app, account, key, value.buf.toString(), cb); }) ); } @@ -230,27 +233,18 @@ export const storeEmail = (app, email, to) => { ))); } -export const removeInboxEmail = (app, account, key) => { +export const removeEmail = (app, container, key) => { return app.mutableData.newMutation() .then((mut) => mut.remove(key, 1) - .then(() => account.inbox_md.applyEntriesMutation(mut)) - ) -} - -export const removeArchivedEmail = (app, account, key) => { - return app.mutableData.newMutation() - .then((mut) => account.archive_md.encryptKey(key) - .then((encryptedKey) => mut.remove(encryptedKey, 1)) - .then(() => account.archive_md.applyEntriesMutation(mut)) + .then(() => container.applyEntriesMutation(mut)) ) } export const archiveEmail = (app, account, key) => { let new_entry_key = genRandomEntryKey(); return account.inbox_md.get(key) - .then((encryptedEmailXorName) => decrypt(encryptedEmailXorName.buf.toString(), account.enc_sk, account.enc_pk)) .then((xorName) => app.mutableData.newMutation() - .then((mut) => mut.insert(new_entry_key, xorName) + .then((mut) => mut.insert(new_entry_key, xorName.buf) .then(() => account.archive_md.applyEntriesMutation(mut)) ) ) From 2530f844d9ec14fa9951d704d5a0ba3c8aee2b7e Mon Sep 17 00:00:00 2001 From: Shankar Date: Fri, 19 May 2017 22:27:52 +0530 Subject: [PATCH 21/44] Update to adapt latest saf_app_nodejs (#179) * feat/enc_dec: update to adapt latest saf_app_nodejs Updated to adapt latest safe_app_nodejs default containers encrpt and decrypt feature. * fix/revert_safe_app_deps: revert back to use upstream safe app dependency Reverted back to use upstream safe app dependency --- web_hosting_manager/app/lib/Downloader.js | 6 +- web_hosting_manager/app/lib/api.js | 199 +++++++++++++--------- web_hosting_manager/app/lib/tasks.js | 6 +- 3 files changed, 129 insertions(+), 82 deletions(-) diff --git a/web_hosting_manager/app/lib/Downloader.js b/web_hosting_manager/app/lib/Downloader.js index 65eb916..c79ccc5 100644 --- a/web_hosting_manager/app/lib/Downloader.js +++ b/web_hosting_manager/app/lib/Downloader.js @@ -19,9 +19,9 @@ export default class Downloader { const tokens = this.path.split('/'); const filePath = path.join(getPath(), tokens.pop()); - return safe.auth.getAccessContainerInfo(accessContainers.public) - .then((mdata) => mdata.get(containerPath.dir)) - .then((val) => safe.mutableData.newPublic(val.buf, typetag)) + return safe.auth.getContainer(accessContainers.public) + .then((mdata) => mdata.encryptKey(containerPath.dir).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf))) + .then((val) => safe.mutableData.newPublic(val, typetag)) .then((mdata) => { const nfs = mdata.emulateAs('NFS'); return nfs.fetch(containerPath.file) diff --git a/web_hosting_manager/app/lib/api.js b/web_hosting_manager/app/lib/api.js index cb6b07c..ada9838 100644 --- a/web_hosting_manager/app/lib/api.js +++ b/web_hosting_manager/app/lib/api.js @@ -85,7 +85,7 @@ export const connect = () => { }; export const fetchAccessInfo = () => { - return safe.auth.refreshContainerAccess() + return safe.auth.refreshContainersPermissions() .then(() => safe.auth.canAccessContainer(accessContainers.public)) .then((hasAccess) => { if (!hasAccess) { @@ -105,20 +105,30 @@ export const fetchAccessInfo = () => { }; export const fetchPublicNames = () => { - return safe.auth.getAccessContainerInfo(accessContainers.publicNames) - .then((mdata) => mdata.getKeys()) - .then((keys) => keys.len() - .then((len) => { - if (len === 0) { - console.log('No public Ids found'); - return; - } - return keys.forEach(function (key) { - if (!publicIds[key] || typeof publicIds[key] !== 'object') { - publicIds[key.toString()] = {}; + return safe.auth.getContainer(accessContainers.publicNames) + .then((mdata) => mdata.getKeys() + .then((keys) => keys.len() + .then((len) => { + if (len === 0) { + console.log('No public Ids found'); + return; } - }) - })) + const encPublicIds = []; + return keys.forEach(function (key) { + encPublicIds.push(key); + }) + .then(() => { + return Promise.all(encPublicIds.map((pubId) => { + return mdata.decrypt(pubId) + .then((decKey) => { + const decPubId = decKey.toString() + if (!publicIds[decPubId] || typeof publicIds[decPubId] !== 'object') { + publicIds[decPubId] = {}; + } + }) + })); + }) + }))) .then(() => publicIds); }; @@ -126,14 +136,20 @@ export const fetchServices = () => { const publicNamesKeys = Object.getOwnPropertyNames(publicIds); return Promise.all(publicNamesKeys.map((publicId) => { const services = {}; - return safe.auth.getAccessContainerInfo(accessContainers.publicNames) - .then((mdata) => mdata.getEntries()) + return safe.auth.getContainer(accessContainers.publicNames) + .then((mdata) => mdata.encryptKey(publicId).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf))) // get publicName mdata - .then((entries) => entries.get(publicId)) - .then((value) => safe.mutableData.newPublic(value.buf, typetag)) + // .then((entries) => entries.get(publicId)) + .then((decVal) => { + return safe.mutableData.newPublic(decVal, typetag) + }) // get services .then((mut) => mut.getEntries() - .then((entries) => entries.forEach((key, val, version) => { + .then((entries) => entries.forEach((key, val) => { + const service = key.toString(); + if (service.indexOf('@email') !== -1) { + return; + } services[key.toString()] = val; }))) .then(() => { @@ -149,10 +165,6 @@ export const fetchServices = () => { })).then(() => publicIds); }; -// export const fetchServiceContents = () => { -// return mock(true); -// }; - export const createPublicId = (publicId) => { publicId = publicId.trim(); if (!publicId) { @@ -184,10 +196,12 @@ export const createPublicId = (publicId) => { .then(() => mdata.getNameAndTag()) }) .then((data) => (publicIdName = data.name)) - .then(() => safe.auth.getAccessContainerInfo(accessContainers.publicNames)) + .then(() => safe.auth.getContainer(accessContainers.publicNames)) .then((mdata) => mdata.getEntries() .then((entries) => entries.mutate() - .then((mut) => mut.insert(publicId, publicIdName) + .then((mut) => mdata.encryptKey(publicId) + .then((encKey) => mdata.encryptValue(publicIdName) + .then((encVal) => mut.insert(encKey, encVal))) .then(() => mdata.applyEntriesMutation(mut))))); }; @@ -202,14 +216,23 @@ export const createService = (publicId, service, container) => { return Promise.reject(new Error(I18n.t('messages.cannotBeEmpty', { name: 'Container path' }))); } - return safe.auth.getAccessContainerInfo(accessContainers.publicNames) - .then((mdata) => mdata.getEntries()) - .then((entries) => entries.get(publicId)) - .then((val) => safe.mutableData.newPublic(val.buf, typetag)) - .then((publicIdMData) => publicIdMData.getEntries() - .then((entries) => entries.mutate() - .then((mut) => mut.insert(service, container) - .then(() => publicIdMData.applyEntriesMutation(mut))))); + return safe.auth.getContainer(accessContainers.publicNames) + .then((mdata) => { + return mdata.encryptKey(publicId).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf)); + }) + .then((val) => { + return safe.mutableData.newPublic(val, typetag); + }) + .then((publicIdMData) => { + return publicIdMData.getEntries() + .then((entries) => { + return entries.mutate() + .then((mut) => { + return mut.insert(service, container) + .then(() => publicIdMData.applyEntriesMutation(mut)); + }); + }) + }); }; export const deleteService = (publicId, service) => { @@ -224,19 +247,35 @@ export const deleteService = (publicId, service) => { export const createContainer = (path) => { return safe.mutableData.newRandomPublic(typetag) - .then((mdata) => mdata.quickSetup({}) - .then(() => mdata.getNameAndTag())) - .then((data) => safe.auth.getAccessContainerInfo(accessContainers.public) - .then((mdata) => mdata.getEntries() - .then((entries) => entries.mutate() - .then((mut) => mut.insert(path, data.name) - .then(() => mdata.applyEntriesMutation(mut))))) - .then(() => data.name)); + .then((mdata) => { + return mdata.quickSetup({}) + .then(() => { + return mdata.getNameAndTag(); + }) + }) + .then((data) => { + return safe.auth.getContainer(accessContainers.public) + .then((mdata) => { + return mdata.getEntries() + .then((entries) => { + return entries.mutate() + .then((mut) => { + return mdata.encryptKey(path) + .then((encKey) => mdata.encryptValue(data.name) + .then((encVal) => mut.insert(encKey, encVal))) + .then(() => mdata.applyEntriesMutation(mut)); + }); + }); + }) + .then(() => { + return data.name; + }); + }); }; export const getPublicContainers = () => { const publicKeys = []; - return safe.auth.getAccessContainerInfo(accessContainers.public) + return safe.auth.getContainer(accessContainers.public) .then((mdata) => mdata.getKeys()) .then((keys) => { return keys.len() @@ -260,39 +299,41 @@ export const deleteItem = (nwPath) => { fileName = path.basename(nwPath); } - return safe.auth.getAccessContainerInfo(accessContainers.public) - .then((mdata) => mdata.getEntries() - .then((entries) => { - if (fileName) { - return entries.get(dirName) - .then((val) => { - return safe.mutableData.newPublic(val.buf, typetag) - .then((dirMdata) => dirMdata.getEntries() - .then(() => dirMdata.getEntries() - .then((dirEntries) => dirEntries.get(fileName) - .then((val) => dirEntries.mutate() - .then((mut) => mut.remove(fileName, val.version + 1) - .then(() => dirMdata.applyEntriesMutation(mut))))))) - }); - } else { - return entries.get(nwPath) + return safe.auth.getContainer(accessContainers.public) + // .then((mdata) => mdata.getEntries() + .then((mdata) => { + if (fileName) { + return mdata.encryptKey(dirName).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf)) + .then((val) => { + return safe.mutableData.newPublic(val, typetag) + .then((dirMdata) => dirMdata.getEntries() + .then(() => dirMdata.getEntries() + .then((dirEntries) => dirEntries.get(fileName) + .then((val) => dirEntries.mutate() + .then((mut) => mut.remove(fileName, val.version + 1) + .then(() => dirMdata.applyEntriesMutation(mut))))))) + }); + } else { + return mdata.getEntries() + .then((entries) => mdata.encryptKey(nwPath).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf) .then((val) => entries.mutate() - .then((mut) => mut.remove(nwPath, val.version + 1) - .then(() => mdata.applyEntriesMutation(mut)))); - } - })); + .then((mut) => mdata.encryptKey(nwPath) + .then((encKey) => mut.remove(encKey, value.version + 1)) + .then(() => mdata.applyEntriesMutation(mut)))))); + } + }); }; export const remapService = (service, publicId, container) => { let containerName = null; - return safe.auth.getAccessContainerInfo(accessContainers.public) - .then((mdata) => mdata.getEntries()) - .then((entries) => entries.get(container)) - .then((val) => (containerName = val.buf)) - .then(() => safe.auth.getAccessContainerInfo(accessContainers.publicNames)) - .then((mdata) => mdata.getEntries()) - .then((entries) => entries.get(publicId)) - .then((val) => safe.mutableData.newPublic(val.buf, typetag)) + return safe.auth.getContainer(accessContainers.public) + .then((mdata) => mdata.encryptKey(container).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf))) + // .then((entries) => entries.get(container)) + .then((val) => (containerName = val)) + .then(() => safe.auth.getContainer(accessContainers.publicNames)) + .then((mdata) => mdata.encryptKey(publicId).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf))) + // .then((entries) => entries.get(publicId)) + .then((val) => safe.mutableData.newPublic(val, typetag)) .then((publicIdMData) => publicIdMData.getEntries() .then(() => publicIdMData.getEntries() .then((entries) => entries.get(service) @@ -303,10 +344,11 @@ export const remapService = (service, publicId, container) => { export const getContainer = (path) => { let result = []; - return safe.auth.getAccessContainerInfo(accessContainers.public) - .then((mdata) => mdata.getEntries()) - .then((entries) => entries.get(path.split('/').slice(0, 3).join('/'))) - .then((val) => safe.mutableData.newPublic(val.buf, typetag)) + return safe.auth.getContainer(accessContainers.public) + .then((mdata) => mdata.encryptKey(path.split('/').slice(0, 3).join('/')) + .then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf))) + // .then((entries) => entries.get(path.split('/').slice(0, 3).join('/'))) + .then((val) => safe.mutableData.newPublic(val, typetag)) .then((mdata) => { const files = []; const nfs = mdata.emulateAs('NFS'); @@ -337,7 +379,11 @@ export const getContainer = (path) => { .then((i) => i.read()) .then((co) => { const dirName = path.split('/').slice(3).join('/'); - result.unshift({ isFile: true, name: dirName ? file.substr(dirName.length + 1) : file, size: co.length }); + result.unshift({ + isFile: true, + name: dirName ? file.substr(dirName.length + 1) : file, + size: co.length + }); }); })); }); @@ -365,9 +411,10 @@ export const cancelDownload = () => { const getContainerName = (mdataName) => { let res = null; - return safe.auth.getAccessContainerInfo(accessContainers.public) + return safe.auth.getContainer(accessContainers.public) .then((mdata) => mdata.getEntries() .then((entries) => entries.forEach((key, val) => { + debugger if (val.buf.equals(mdataName.buf)) { res = key.toString(); } diff --git a/web_hosting_manager/app/lib/tasks.js b/web_hosting_manager/app/lib/tasks.js index d2af2cd..fbcbb76 100644 --- a/web_hosting_manager/app/lib/tasks.js +++ b/web_hosting_manager/app/lib/tasks.js @@ -36,9 +36,9 @@ export class FileUploadTask extends Task { } const containerPath = parseContainerPath(this.networkPath); - return safe.auth.getAccessContainerInfo(accessContainers.public) - .then((mdata) => mdata.get(containerPath.target)) - .then((val) => safe.mutableData.newPublic(val.buf, typetag)) + return safe.auth.getContainer(accessContainers.public) + .then((mdata) => mdata.encryptKey(containerPath.target).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf))) + .then((val) => safe.mutableData.newPublic(val, typetag)) .then((mdata) => { const nfs = mdata.emulateAs('NFS'); return nfs.create(fs.readFileSync(this.localPath)) From 63a30a40cac29dd8f38ffedcd5e1012ba019b033 Mon Sep 17 00:00:00 2001 From: Gabriel Viganotti Date: Mon, 22 May 2017 01:17:19 -0400 Subject: [PATCH 22/44] fix/Adapt to latest safe_app_nodejs and minor fix (#178) * feat/Adapt to a change in safe_app_nodejs which renames a function * fix/Minor fix when creating the email service entry in the services MD * feat/Changes due to functions renamed in safe_app_nodejs --- email_app/app/safenet_comm.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/email_app/app/safenet_comm.js b/email_app/app/safenet_comm.js index 05d1789..62e3348 100644 --- a/email_app/app/safenet_comm.js +++ b/email_app/app/safenet_comm.js @@ -42,7 +42,7 @@ export const authApp = () => { let uri = getAuthData(); if (uri) { return fromAuthURI(APP_INFO.info, uri) - .then((registered_app) => registered_app.auth.refreshContainerAccess() + .then((registered_app) => registered_app.auth.refreshContainersPermissions() .then(() => registered_app) ) .catch((err) => { @@ -60,7 +60,7 @@ export const connect = (uri) => { return fromAuthURI(APP_INFO.info, uri) .then((app) => registered_app = app) .then(() => saveAuthData(uri)) - .then(() => registered_app.auth.refreshContainerAccess()) + .then(() => registered_app.auth.refreshContainersPermissions()) .then(() => registered_app); } @@ -191,10 +191,9 @@ export const setupAccount = (app, emailId) => { enc_sk: key_pair.privateKey, enc_pk: key_pair.publicKey}) .then(() => newAccount.inbox_md.serialise()) .then((md_serialised) => inbox_serialised = md_serialised) - .then(() => app.auth.getAccessContainerInfo(APP_INFO.containers.publicNames)) - .then((pub_names_md) => pub_names_md.encryptKey(serviceInfo.publicId) - .then((encrypted_publicId) => pub_names_md.get(encrypted_publicId)) - .then((services) => addService(app, serviceInfo, inbox_serialised) + .then(() => app.auth.getContainer(APP_INFO.containers.publicNames)) + .then((pub_names_md) => pub_names_md.get(serviceInfo.publicId) + .then((services) => addEmailService(app, serviceInfo, inbox_serialised) , (err) => { if (err.name === 'ERR_NO_SUCH_ENTRY') { return createPublicIdAndEmailService(app, pub_names_md, From 436a05d053106a23ae68d1acff73a720b62679b4 Mon Sep 17 00:00:00 2001 From: Shankar Date: Mon, 22 May 2017 10:48:03 +0530 Subject: [PATCH 23/44] fix/read_file: resolve read file issue (#176) Resolved read file issue and typo changed from Launcher to Authenticator --- markdown_editor/src/components/app.js | 2 +- markdown_editor/src/store.js | 34 ++++----------------------- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/markdown_editor/src/components/app.js b/markdown_editor/src/components/app.js index 27c624b..876eec7 100644 --- a/markdown_editor/src/components/app.js +++ b/markdown_editor/src/components/app.js @@ -42,7 +42,7 @@ export default class App extends Component { render() { - let sub =

    Please authorise the app in Launcher.

    ; + let sub =

    Please authorise the app in Authenticator.

    ; if (this.state.authorised) { if (this.state.selectedFile) { sub = { return new Buffer(JSON.stringify(oldData)); }; -const _getHomeContainer = () => { - if (HOME_CONTAINER_HANDLE) { - return Promise.resolve(HOME_CONTAINER_HANDLE); - } - return window.safeApp.getHomeContainer(ACCESS_TOKEN) - .then((mdata) => (HOME_CONTAINER_HANDLE = mdata)) - .then(() => HOME_CONTAINER_HANDLE); -}; - /** * Connect to safe network with response URI from Authenticator * @param token @@ -96,19 +86,6 @@ const _connectAuthorised = (token, resUri) => { .then((token) => (ACCESS_TOKEN = token)); }; -/** - * Check permission for granted access containers - * @private - */ -const _fetchAccessInfo = () => { - return window.safeApp.canAccessContainer(ACCESS_TOKEN, '_public') - .then((hasAccess) => { - if (!hasAccess) { - throw new Error('Cannot access PUBLIC Container'); - } - return true; - }); -}; /** * Read file @@ -138,7 +115,7 @@ const _getFile = (mdata, filename) => { * @private */ const _updateFile = (filename, payload) => { - return _getHomeContainer() + return window.safeApp.getHomeContainer(ACCESS_TOKEN) .then((mdata) => { return _getFile(mdata, filename) .then((files) => window.safeMutableData.emulateAs(ACCESS_TOKEN, mdata, 'NFS') @@ -153,7 +130,7 @@ const _updateFile = (filename, payload) => { * @param version */ export const readFile = (filename, version) => { - return _getHomeContainer() + return window.safeApp.getHomeContainer(ACCESS_TOKEN) .then((mdata) => _getFile(mdata, filename)) .then((file) => { return version ? file.data[version] : file.data @@ -171,7 +148,7 @@ export const saveFile = (filename, data) => { console.log("existing"); return _updateFile(filename, data); } else { - return _getHomeContainer() + return window.safeApp.getHomeContainer(ACCESS_TOKEN) .then((mdata) => window.safeMutableData.emulateAs(ACCESS_TOKEN, mdata, 'NFS') .then((nfs) => window.safeNfs.create(ACCESS_TOKEN, nfs, _prepareFile([], data)) .then((file) => window.safeNfs.insert(ACCESS_TOKEN, nfs, file, filename))) @@ -202,7 +179,7 @@ export const getFileVersions = (filename) => { export const getFileIndex = () => { if (FILE_INDEX) return Promise.resolve(FILE_INDEX); - return _getHomeContainer() + return window.safeApp.getHomeContainer(ACCESS_TOKEN) .then((mdata) => window.safeMutableData.encryptKey(ACCESS_TOKEN, mdata, FILE_INDEX_KEY) .then((key) => window.safeMutableData.get(ACCESS_TOKEN, mdata, key)) .then((fileIndex) => { @@ -241,6 +218,5 @@ export const authorise = () => { _saveResponseUri(resUri); return _connectAuthorised(token, resUri); }); - }) - .then(() => _fetchAccessInfo()); + }); }; From df52b0582655035b63f08f6688779dd27d374f9a Mon Sep 17 00:00:00 2001 From: Gabriel Viganotti Date: Fri, 26 May 2017 15:02:56 +0900 Subject: [PATCH 24/44] feat/Encrypt entries in `_publicNames` container (#181) --- email_app/app/safenet_comm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/email_app/app/safenet_comm.js b/email_app/app/safenet_comm.js index 62e3348..ee9d456 100644 --- a/email_app/app/safenet_comm.js +++ b/email_app/app/safenet_comm.js @@ -172,7 +172,7 @@ const createPublicIdAndEmailService = (app, pub_names_md, serviceInfo, return addEmailService(app, serviceInfo, inbox_serialised) .then((md) => md.getNameAndTag()) .then((services) => app.mutableData.newMutation() - .then((mut) => mut.insert(serviceInfo.publicId, services.name) + .then((mut) => insertEncrypted(pub_names_md, mut, serviceInfo.publicId, services.name) .then(() => pub_names_md.applyEntriesMutation(mut)) )) } @@ -192,7 +192,7 @@ export const setupAccount = (app, emailId) => { .then(() => newAccount.inbox_md.serialise()) .then((md_serialised) => inbox_serialised = md_serialised) .then(() => app.auth.getContainer(APP_INFO.containers.publicNames)) - .then((pub_names_md) => pub_names_md.get(serviceInfo.publicId) + .then((pub_names_md) => pub_names_md.encryptKey(serviceInfo.publicId).then((key) => pub_names_md.get(key)) .then((services) => addEmailService(app, serviceInfo, inbox_serialised) , (err) => { if (err.name === 'ERR_NO_SUCH_ENTRY') { From f69bcb910aa94d38e268266bd7245d00225cc1da Mon Sep 17 00:00:00 2001 From: Shankar Date: Fri, 26 May 2017 11:33:57 +0530 Subject: [PATCH 25/44] fix/UI: resolve ui related issues (#182) Resolved UI related issues --- web_hosting_manager/app/actions/app.js | 22 +++-- .../app/components/CreateService.js | 4 + .../app/components/FileExplorer.js | 4 + web_hosting_manager/app/lib/api.js | 78 +++++++++++++---- web_hosting_manager/app/lib/tasks.js | 15 +++- .../app/reducers/access_info.js | 9 +- .../app/reducers/containers.js | 25 +++++- web_hosting_manager/app/reducers/file.js | 6 ++ web_hosting_manager/app/reducers/public_id.js | 84 ++++++++++++++++--- web_hosting_manager/app/reducers/service.js | 15 +++- web_hosting_manager/app/styles/app.css | 2 +- web_hosting_manager/app/utils/app_utils.js | 5 ++ 12 files changed, 224 insertions(+), 45 deletions(-) create mode 100644 web_hosting_manager/app/utils/app_utils.js diff --git a/web_hosting_manager/app/actions/app.js b/web_hosting_manager/app/actions/app.js index 3da1b46..8a59eab 100644 --- a/web_hosting_manager/app/actions/app.js +++ b/web_hosting_manager/app/actions/app.js @@ -30,6 +30,7 @@ export const DOWNLOAD_STARTED = 'DOWNLOAD_STARTED'; export const DOWNLOADING = 'DOWNLOADING'; export const DOWNLOAD_FAILED = 'DOWNLOAD_FAILED'; export const DOWNLOAD_COMPLETED = 'DOWNLOAD_COMPLETED'; +export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION'; export const DELETE = 'DELETE'; @@ -99,14 +100,17 @@ export const createContainerAndService = (publicId: string, service: string, const path = `${parentConatiner}/${publicId}/${conatinerName}`; return { type: CREATE_CONTAINER_AND_SERVICE, - payload: api.createContainer(path) - .then((name) => { - return api.createService(publicId, service, name); + payload: api.checkServiceExist(publicId, service, path) + .then((exist) => { + if (!exist) { + return api.createContainer(path) + .then((name) => { + return api.createService(publicId, service, name); + }); + } + return Promise.resolve(true); }) .then(() => api.fetchServices()) - .then(() => { - return conatinerName; - }) }; }; @@ -226,3 +230,9 @@ export const deleteItem = (containerPath, name) => { }) }; }; + +export const clearNotification = () => { + return { + type: CLEAR_NOTIFICATION + } +}; diff --git a/web_hosting_manager/app/components/CreateService.js b/web_hosting_manager/app/components/CreateService.js index ca8333b..318bd08 100644 --- a/web_hosting_manager/app/components/CreateService.js +++ b/web_hosting_manager/app/components/CreateService.js @@ -48,6 +48,10 @@ export default class CreateService extends Component { } } + componentWillUnmount() { + this.props.clearNotification(); + } + validate() { let valid = true; const serviceErrorMsg = I18n.t('messages.emptyServiceName'); diff --git a/web_hosting_manager/app/components/FileExplorer.js b/web_hosting_manager/app/components/FileExplorer.js index 0290d60..98aef90 100644 --- a/web_hosting_manager/app/components/FileExplorer.js +++ b/web_hosting_manager/app/components/FileExplorer.js @@ -59,6 +59,10 @@ export default class FileExplorer extends Component { } } + componentWillUnmount() { + this.props.clearNotification(); + } + bytesToSize(bytes) { const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; if (bytes === 0) { diff --git a/web_hosting_manager/app/lib/api.js b/web_hosting_manager/app/lib/api.js index ada9838..b58af2e 100644 --- a/web_hosting_manager/app/lib/api.js +++ b/web_hosting_manager/app/lib/api.js @@ -138,19 +138,16 @@ export const fetchServices = () => { const services = {}; return safe.auth.getContainer(accessContainers.publicNames) .then((mdata) => mdata.encryptKey(publicId).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf))) - // get publicName mdata - // .then((entries) => entries.get(publicId)) .then((decVal) => { return safe.mutableData.newPublic(decVal, typetag) }) - // get services .then((mut) => mut.getEntries() .then((entries) => entries.forEach((key, val) => { const service = key.toString(); - if (service.indexOf('@email') !== -1) { + if ((service.indexOf('@email') !== -1) || (val.buf.length === 0)) { return; } - services[key.toString()] = val; + services[service] = val; }))) .then(() => { return Promise.all(Object.keys(services).map((key) => { @@ -314,12 +311,29 @@ export const deleteItem = (nwPath) => { .then(() => dirMdata.applyEntriesMutation(mut))))))) }); } else { - return mdata.getEntries() - .then((entries) => mdata.encryptKey(nwPath).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf) - .then((val) => entries.mutate() - .then((mut) => mdata.encryptKey(nwPath) - .then((encKey) => mut.remove(encKey, value.version + 1)) - .then(() => mdata.applyEntriesMutation(mut)))))); + return mdata.encryptKey(nwPath.split('/').slice(0, -1).join('/')).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf)) + .then((val) => { + return safe.mutableData.newPublic(val, typetag) + .then((tarMdata) => { + const targetKeys = []; + return tarMdata.getEntries() + .then((entries) => entries.forEach((key, val) => { + const keyStr = key.toString(); + const dirname = nwPath.split('/').slice(-1).toString() + if (keyStr.indexOf(dirname) !== 0) { + return; + } + targetKeys.push({key: keyStr, version: val.version}); + }) + .then(() => Promise.all(targetKeys.map((tar) => { + return entries.mutate() + .then((mut) => { + return mut.remove(tar.key, tar.version + 1) + .then(() => tarMdata.applyEntriesMutation(mut)) + }); + })))); + }); + }); } }); }; @@ -352,8 +366,11 @@ export const getContainer = (path) => { .then((mdata) => { const files = []; const nfs = mdata.emulateAs('NFS'); - return mdata.getKeys() - .then((keys) => keys.forEach((key) => { + return mdata.getEntries() + .then((entries) => entries.forEach((key, value) => { + if (value.buf.length === 0) { + return + } let keyStr = key.toString(); const rootName = path.split('/').slice(3).join('/'); if (rootName && (keyStr.indexOf(rootName) !== 0)) { @@ -409,12 +426,45 @@ export const cancelDownload = () => { downloader.cancel(); }; +export const checkServiceExist = (publicId, service, path) => { + return safe.auth.getContainer(accessContainers.publicNames) + .then((mdata) => mdata.encryptKey(publicId).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf))) + .then((decVal) => { + return safe.mutableData.newPublic(decVal, typetag) + }) + .then((pubMut) => { + return pubMut.get(service) + .then((value) => { + if (value.buf.length !== 0) { + return; + } + return safe.auth.getContainer(accessContainers.public) + .then((mdata) => { + return mdata.encryptKey(path).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf)) + }) + .then((val) => { + return pubMut.getEntries() + .then((entries) => { + return entries.mutate().then((mut) => { + return mut.update(service, val, value.version + 1).then(() => pubMut.applyEntriesMutation(mut)); + }); + }); + }); + }) + .catch((err) => { + if (err.name === 'ERR_NO_SUCH_ENTRY') { + return Promise.resolve(false); + } + return Promise.reject(); + }); + }); +}; + const getContainerName = (mdataName) => { let res = null; return safe.auth.getContainer(accessContainers.public) .then((mdata) => mdata.getEntries() .then((entries) => entries.forEach((key, val) => { - debugger if (val.buf.equals(mdataName.buf)) { res = key.toString(); } diff --git a/web_hosting_manager/app/lib/tasks.js b/web_hosting_manager/app/lib/tasks.js index fbcbb76..da806ee 100644 --- a/web_hosting_manager/app/lib/tasks.js +++ b/web_hosting_manager/app/lib/tasks.js @@ -42,7 +42,19 @@ export class FileUploadTask extends Task { .then((mdata) => { const nfs = mdata.emulateAs('NFS'); return nfs.create(fs.readFileSync(this.localPath)) - .then((file) => nfs.insert(containerPath.file, file)); + .then((file) => nfs.insert(containerPath.file, file) + .catch((err) => { + if (err.name !== 'ERR_ENTRY_EXISTS') { + return Promise.reject(err); + } + return mdata.get(containerPath.file) + .then((value) => { + if (value.buf.length !== 0) { + return Promise.reject(err); + } + return nfs.update(containerPath.file, file, value.version + 1); + }); + })); }) .then(() => callback(null, { isFile: true, @@ -52,3 +64,4 @@ export class FileUploadTask extends Task { .catch(callback); } } + diff --git a/web_hosting_manager/app/reducers/access_info.js b/web_hosting_manager/app/reducers/access_info.js index 77432e5..7eaf76c 100644 --- a/web_hosting_manager/app/reducers/access_info.js +++ b/web_hosting_manager/app/reducers/access_info.js @@ -2,11 +2,12 @@ import * as Action from '../actions/app'; import { I18n } from 'react-redux-i18n'; +import { trimErrorMsg } from '../utils/app_utils'; const initialState = { - fetchingAccessInfo: false, - fetchedAccessInfo: false, - error: null + fetchingAccessInfo: false, + fetchedAccessInfo: false, + error: null }; const accessInfo = (state: Object = initialState, action: Object) => { @@ -37,7 +38,7 @@ const accessInfo = (state: Object = initialState, action: Object) => { state = { ...state, fetchingAccessInfo: false, - error: I18n.t('messages.fetchingAccessFailed', { error: action.payload.message }) + error: I18n.t('messages.fetchingAccessFailed', { error: trimErrorMsg(action.payload.message) }) }; break; } diff --git a/web_hosting_manager/app/reducers/containers.js b/web_hosting_manager/app/reducers/containers.js index 722849f..2fad4b0 100644 --- a/web_hosting_manager/app/reducers/containers.js +++ b/web_hosting_manager/app/reducers/containers.js @@ -1,5 +1,6 @@ import * as Action from '../actions/app'; import { I18n } from 'react-redux-i18n'; +import { trimErrorMsg } from '../utils/app_utils'; const initialState = { fetchingPublicContainers: false, @@ -41,13 +42,23 @@ const containers = (state: Object = initialState, action: Object) => { state = { ...state, fetchedPublicContainers: false, - error: I18n.t('messages.fetchingPublicContainerFailed', { error: action.payload.message }) + error: I18n.t('messages.fetchingPublicContainerFailed', { error: trimErrorMsg(action.payload.message) }) }; break; case `${Action.CREATE_CONTAINER_AND_SERVICE}_FULFILLED`: const copy = state.publicContainers.map((name) => { return name; }); - copy.push(action.payload); + // copy.push(action.payload); + let key = null; + for (key of Object.keys(action.payload)) { + let skey = null; + for (skey of Object.keys(action.payload[key])) { + const contName = action.payload[key][skey]; + if (copy.indexOf(contName) === -1) { + copy.push(contName); + } + } + } state = { ...state, publicContainers: copy @@ -75,7 +86,7 @@ const containers = (state: Object = initialState, action: Object) => { ...state, fetchingContainer: false, containerInfo: [], - error: I18n.t('messages.fetchingContainerFailed', { error: action.payload.message }) + error: I18n.t('messages.fetchingContainerFailed', { error: trimErrorMsg(action.payload.message) }) }; break; @@ -98,7 +109,13 @@ const containers = (state: Object = initialState, action: Object) => { state = { ...state, deleting: false, - error: action.payload.message + error: trimErrorMsg(action.payload.message) + }; + break; + case Action.CLEAR_NOTIFICATION: + state = { + ...state, + error: undefined }; break; } diff --git a/web_hosting_manager/app/reducers/file.js b/web_hosting_manager/app/reducers/file.js index 0e9a669..1577a8e 100644 --- a/web_hosting_manager/app/reducers/file.js +++ b/web_hosting_manager/app/reducers/file.js @@ -79,6 +79,12 @@ const file = (state: Object = initialState, action: Object) => { error: action.payload.message }; break; + case Action.CLEAR_NOTIFICATION: + state = { + ...state, + error: undefined + }; + break; } return state; }; diff --git a/web_hosting_manager/app/reducers/public_id.js b/web_hosting_manager/app/reducers/public_id.js index be04606..cec9f34 100644 --- a/web_hosting_manager/app/reducers/public_id.js +++ b/web_hosting_manager/app/reducers/public_id.js @@ -2,13 +2,14 @@ import * as Action from '../actions/app'; import { I18n } from 'react-redux-i18n'; +import { trimErrorMsg } from '../utils/app_utils'; const initialState = { - fetchingPublicNames: false, - fetchedPublicNames: false, - publicNames: {}, - creatingPublicId: false, - error: null + fetchingPublicNames: false, + fetchedPublicNames: false, + publicNames: {}, + creatingPublicId: false, + error: null }; const publicId = (state: Object = initialState, action: Object) => { @@ -23,18 +24,25 @@ const publicId = (state: Object = initialState, action: Object) => { case `${Action.FETCH_PUBLIC_NAMES}_PENDING`: state = { ...state, - publicNames: [], + publicNames: {}, fetchingPublicNames: true }; break; case `${Action.FETCH_PUBLIC_NAMES}_FULFILLED`: + { + const publicNames = {}; + let pkey = null; + for (pkey of Object.keys(action.payload)) { + publicNames[pkey] = { ...action.payload[pkey] }; + } state = { ...state, fetchingPublicNames: false, fetchedPublicNames: true, - publicNames: action.payload + publicNames }; + } break; case `${Action.FETCH_PUBLIC_NAMES}_REJECTED`: @@ -42,7 +50,7 @@ const publicId = (state: Object = initialState, action: Object) => { ...state, fetchingPublicNames: false, publicNames: {}, - error: I18n.t('messages.fetchingPublicNamesFailed', { error: action.payload.message }) + error: I18n.t('messages.fetchingPublicNamesFailed', { error: trimErrorMsg(action.payload.message) }) }; break; @@ -54,26 +62,80 @@ const publicId = (state: Object = initialState, action: Object) => { break; case `${Action.CREATE_PUBLIC_ID}_FULFILLED`: + { + const publicNames = {}; + let pkey = null; + for (pkey of Object.keys(action.payload)) { + publicNames[pkey] = { ...action.payload[pkey] }; + } state = { ...state, creatingPublicId: false, - publicNames: action.payload, + publicNames, error: '' }; + } break; case `${Action.CREATE_PUBLIC_ID}_REJECTED`: state = { ...state, creatingPublicId: false, - error: action.payload.message + error: trimErrorMsg(action.payload.message) + }; + break; + + case `${Action.DELETE_SERVICE}_FULFILLED`: + { + const publicNames = {}; + let pkey = null; + for (pkey of Object.keys(action.payload)) { + publicNames[pkey] = { ...action.payload[pkey] }; + } + state = { + ...state, + publicNames + }; + } + break; + + case `${Action.CREATE_CONTAINER_AND_SERVICE}_FULFILLED`: + { + const publicNames = {}; + let pkey = null; + for (pkey of Object.keys(action.payload)) { + publicNames[pkey] = { ...action.payload[pkey] }; + } + state = { + ...state, + publicNames + }; + } + break; + + case `${Action.FETCH_SERVICES}_FULFILLED`: + { + const publicNames = {}; + let pkey = null; + for (pkey of Object.keys(action.payload)) { + publicNames[pkey] = { ...action.payload[pkey] }; + } + state = { + ...state, + publicNames }; + } break; case `${Action.REMAP_SERVICE}_FULFILLED`: + const publicNames = {}; + let pkey = null; + for (pkey of Object.keys(action.payload)) { + publicNames[pkey] = { ...action.payload[pkey] }; + } state = { ...state, - publicNames: action.payload + publicNames }; break; } diff --git a/web_hosting_manager/app/reducers/service.js b/web_hosting_manager/app/reducers/service.js index 41f36ee..eaa045b 100644 --- a/web_hosting_manager/app/reducers/service.js +++ b/web_hosting_manager/app/reducers/service.js @@ -2,6 +2,7 @@ import * as Action from '../actions/app'; import { I18n } from 'react-redux-i18n'; +import { trimErrorMsg } from '../utils/app_utils'; const initialState = { fetchingServices: false, @@ -40,7 +41,7 @@ const service = (state: Object = initialState, action: Object) => { state = { ...state, fetchingServices: false, - error: I18n.t('messages.fetchingServicesFailed', { error: action.payload.message }) + error: I18n.t('messages.fetchingServicesFailed', { error: trimErrorMsg(action.payload.message) }) }; break; @@ -63,7 +64,7 @@ const service = (state: Object = initialState, action: Object) => { state = { ...state, creatingService: false, - error: action.payload.message + error: trimErrorMsg(action.payload.message) }; break; @@ -87,7 +88,7 @@ const service = (state: Object = initialState, action: Object) => { state = { ...state, creatingService: false, - error: action.payload.message + error: trimErrorMsg(action.payload.message) }; break; @@ -111,7 +112,13 @@ const service = (state: Object = initialState, action: Object) => { state = { ...state, remapping: false, - error: action.payload.message + error: trimErrorMsg(action.payload.message) + }; + break; + case Action.CLEAR_NOTIFICATION: + state = { + ...state, + error: undefined }; break; } diff --git a/web_hosting_manager/app/styles/app.css b/web_hosting_manager/app/styles/app.css index 27065f7..437d1a6 100644 --- a/web_hosting_manager/app/styles/app.css +++ b/web_hosting_manager/app/styles/app.css @@ -391,6 +391,7 @@ nav { color: white; padding: 8px 16px; font-size: 14px; + height: auto; } .custom-btn-grp .ant-select { @@ -474,4 +475,3 @@ nav { transform: rotate(360deg); } } - diff --git a/web_hosting_manager/app/utils/app_utils.js b/web_hosting_manager/app/utils/app_utils.js new file mode 100644 index 0000000..f8c5fca --- /dev/null +++ b/web_hosting_manager/app/utils/app_utils.js @@ -0,0 +1,5 @@ +export const trimErrorMsg = (msg) => { + let index = msg.indexOf('->'); + index = (index === -1) ? index : index + 2; + return msg.slice(index).trim() +}; From 71699bd588ff96ec4ae557603ab8b3b2b6936ac6 Mon Sep 17 00:00:00 2001 From: Gabriel Viganotti Date: Mon, 5 Jun 2017 16:55:18 +0900 Subject: [PATCH 26/44] fix/Minor fix to make use of error codes rather than error names/messages to control some flow (#184) --- email_app/app/components/create_account.js | 2 +- email_app/app/safenet_comm.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/email_app/app/components/create_account.js b/email_app/app/components/create_account.js index 0ae6e05..e17ac9f 100644 --- a/email_app/app/components/create_account.js +++ b/email_app/app/components/create_account.js @@ -32,7 +32,7 @@ export default class CreateAccount extends Component { return createAccount(emailId) .then(this.storeCreatedAccount) .catch((err) => { - if (err.name === 'ERR_DATA_EXISTS') { + if (err.code === -104) { return createAccountError(new Error(MESSAGES.EMAIL_ALREADY_TAKEN)); } return createAccountError(err); diff --git a/email_app/app/safenet_comm.js b/email_app/app/safenet_comm.js index ee9d456..4b9ac50 100644 --- a/email_app/app/safenet_comm.js +++ b/email_app/app/safenet_comm.js @@ -195,7 +195,7 @@ export const setupAccount = (app, emailId) => { .then((pub_names_md) => pub_names_md.encryptKey(serviceInfo.publicId).then((key) => pub_names_md.get(key)) .then((services) => addEmailService(app, serviceInfo, inbox_serialised) , (err) => { - if (err.name === 'ERR_NO_SUCH_ENTRY') { + if (err.code === -106) { return createPublicIdAndEmailService(app, pub_names_md, serviceInfo, inbox_serialised); } From 8a1a01fd9def7abeb1e256736ca6869e68e0877a Mon Sep 17 00:00:00 2001 From: Shankar Date: Thu, 8 Jun 2017 09:54:22 +0530 Subject: [PATCH 27/44] fix/auth_response: update to handle auth response (#185) Updated to handle auth response and revoke. --- web_hosting_manager/app/actions/app.js | 25 ++++++++--- web_hosting_manager/app/components/Auth.js | 15 ++++++- .../app/components/CreateService.js | 6 +++ .../app/components/FileExplorer.js | 3 ++ web_hosting_manager/app/components/Home.js | 4 +- .../app/containers/AuthPage.js | 1 + .../app/containers/CreateService.js | 3 +- .../app/containers/FileExplorer.js | 3 +- .../app/containers/HomePage.js | 3 +- web_hosting_manager/app/index.js | 7 +--- web_hosting_manager/app/lib/api.js | 41 +++++++++++++++---- web_hosting_manager/app/locales/en.js | 3 +- web_hosting_manager/app/reducers/auth.js | 12 +++++- .../app/styles/ant_customisation.css | 4 ++ web_hosting_manager/app/styles/app.css | 9 ++++ web_hosting_manager/package.json | 2 +- 16 files changed, 112 insertions(+), 29 deletions(-) diff --git a/web_hosting_manager/app/actions/app.js b/web_hosting_manager/app/actions/app.js index 8a59eab..faf9571 100644 --- a/web_hosting_manager/app/actions/app.js +++ b/web_hosting_manager/app/actions/app.js @@ -31,6 +31,7 @@ export const DOWNLOADING = 'DOWNLOADING'; export const DOWNLOAD_FAILED = 'DOWNLOAD_FAILED'; export const DOWNLOAD_COMPLETED = 'DOWNLOAD_COMPLETED'; export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION'; +export const REVOKED = 'REVOKED'; export const DELETE = 'DELETE'; @@ -47,18 +48,30 @@ export const reset = () => { }; }; -export const connect = (forceTempValue: boolean) => { - if (!forceTempValue && !api.hasLocalAuthInfo()) { +export const revoked = () => { + return { + type: REVOKED + }; +}; + +export const connect = (authRes: String) => { + if (!authRes && !api.hasLocalAuthInfo()) { return sendAuthRequest(); } - return { - type: CONNECT, - payload: api.connect() + return (dispatch) => { + return dispatch({ + type: CONNECT, + payload: api.connect(authRes) + .then((resType) => { + if (resType === api.AUTH_RES_TYPES.revoked) { + return dispatch(revoked()); + } + }) + }) }; }; export const onAuthSuccess = (authInfo: Object) => { - api.saveAuthInfo(authInfo); return { type: ON_AUTH_SUCCESS }; diff --git a/web_hosting_manager/app/components/Auth.js b/web_hosting_manager/app/components/Auth.js index cb17e4b..049e410 100644 --- a/web_hosting_manager/app/components/Auth.js +++ b/web_hosting_manager/app/components/Auth.js @@ -36,11 +36,13 @@ export default class Auth extends Component { }; componentDidMount() { - this.props.connect(); + if (!this.props.isRevoked) { + this.props.connect(); + } } componentWillUpdate(props) { - if (props.fetchedServices && !props.serviceError) { + if (!this.props.isRevoked && props.fetchedServices && !props.serviceError) { props.router.replace('/home'); } } @@ -66,6 +68,15 @@ export default class Auth extends Component { } getDisplayContent() { + if (this.props.isRevoked) { + return { + title: I18n.t('label.initialising.revoked'), + content: (
    +
    Application got revoked. Please Reauthorise.
    + +
    ) + }; + } if (this.props.authError) { return { title: I18n.t('label.initialising.authErrorTitle'), diff --git a/web_hosting_manager/app/components/CreateService.js b/web_hosting_manager/app/components/CreateService.js index 318bd08..4e733d5 100644 --- a/web_hosting_manager/app/components/CreateService.js +++ b/web_hosting_manager/app/components/CreateService.js @@ -38,6 +38,12 @@ export default class CreateService extends Component { this.containerName = this.props.publicContainers[0]; } + componentWillUpdate(nextProps) { + if (nextProps.isRevoked) { + nextProps.router.replace('/'); + } + } + componentWillReceiveProps(nextProps) { if (this.props.creatingService && !nextProps.creatingService && nextProps.serviceError) { this.setState({ diff --git a/web_hosting_manager/app/components/FileExplorer.js b/web_hosting_manager/app/components/FileExplorer.js index 98aef90..2e97681 100644 --- a/web_hosting_manager/app/components/FileExplorer.js +++ b/web_hosting_manager/app/components/FileExplorer.js @@ -42,6 +42,9 @@ export default class FileExplorer extends Component { } componentWillUpdate(nextProps) { + if (nextProps.isRevoked) { + nextProps.router.replace('/'); + } if (this.props.fetchingContainer && !nextProps.fetchingContainer && this.fetchingContainerPath) { if (!nextProps.containerError) { diff --git a/web_hosting_manager/app/components/Home.js b/web_hosting_manager/app/components/Home.js index 0d20bb8..ff53954 100644 --- a/web_hosting_manager/app/components/Home.js +++ b/web_hosting_manager/app/components/Home.js @@ -57,7 +57,9 @@ export default class Auth extends Component { } componentWillUpdate(nextProps) { - + if (nextProps.isRevoked) { + nextProps.router.replace('/'); + } if (nextProps.fetchingPublicContainers) { this.reloadingContainer = true; } diff --git a/web_hosting_manager/app/containers/AuthPage.js b/web_hosting_manager/app/containers/AuthPage.js index af8d00c..7c5bb31 100644 --- a/web_hosting_manager/app/containers/AuthPage.js +++ b/web_hosting_manager/app/containers/AuthPage.js @@ -11,6 +11,7 @@ const mapStateToProps = (state) => { isAuthorising: state.auth.isAuthorising, isAuthorised: state.auth.isAuthorised, authError: state.auth.error, + isRevoked: state.auth.isRevoked, isConnecting: state.connection.isConnecting, isConnected: state.connection.isConnected, connectionError: state.connection.error, diff --git a/web_hosting_manager/app/containers/CreateService.js b/web_hosting_manager/app/containers/CreateService.js index a02013e..c10bd04 100644 --- a/web_hosting_manager/app/containers/CreateService.js +++ b/web_hosting_manager/app/containers/CreateService.js @@ -15,7 +15,8 @@ const mapStateToProps = (state) => { serviceError: state.service.error, fetchingPublicContainers: state.containers.fetchingPublicContainers, publicContainers: state.containers.publicContainers, - publicContainersError: state.containers.error + publicContainersError: state.containers.error, + isRevoked: state.auth.isRevoked }; }; diff --git a/web_hosting_manager/app/containers/FileExplorer.js b/web_hosting_manager/app/containers/FileExplorer.js index 1890f4d..d27df14 100644 --- a/web_hosting_manager/app/containers/FileExplorer.js +++ b/web_hosting_manager/app/containers/FileExplorer.js @@ -19,7 +19,8 @@ const mapStateToProps = (state) => { uploadStatus: state.file.uploadStatus, downloading: state.file.downloading, downloadProgress: state.file.downloadProgress, - fileError: state.file.error + fileError: state.file.error, + isRevoked: state.auth.isRevoked }; }; diff --git a/web_hosting_manager/app/containers/HomePage.js b/web_hosting_manager/app/containers/HomePage.js index 6df2b32..c4b4ed9 100644 --- a/web_hosting_manager/app/containers/HomePage.js +++ b/web_hosting_manager/app/containers/HomePage.js @@ -21,7 +21,8 @@ const mapStateToProps = (state) => { remapping: state.service.remapping, serviceError: state.service.error, publicContainers: state.containers.publicContainers, - fetchingPublicContainers: state.containers.fetchingPublicContainers + fetchingPublicContainers: state.containers.fetchingPublicContainers, + isRevoked: state.auth.isRevoked }; }; diff --git a/web_hosting_manager/app/index.js b/web_hosting_manager/app/index.js index 5ba5f6b..fb1338a 100644 --- a/web_hosting_manager/app/index.js +++ b/web_hosting_manager/app/index.js @@ -32,12 +32,9 @@ store.dispatch(setLocale(locale)); initTempFolder(); const listenForAuthReponse = (event, response) => { - // TODO parse response if (response) { - store.dispatch(onAuthSuccess({ data: response })); // TODO do it concurrently (no to linked dispatch) - setTimeout(() => { - store.dispatch(connect(true)); - }, 2000) + store.dispatch(onAuthSuccess()); + store.dispatch(connect(response)); } else { store.dispatch(onAuthFailure(new Error('Authorisation failed'))); } diff --git a/web_hosting_manager/app/lib/api.js b/web_hosting_manager/app/lib/api.js index b58af2e..bed83dd 100644 --- a/web_hosting_manager/app/lib/api.js +++ b/web_hosting_manager/app/lib/api.js @@ -38,6 +38,11 @@ const APP_INFO = { } }; +export const AUTH_RES_TYPES = { + containers: 'containers', + revoked: 'revoked' +}; + export const accessContainers = { public: '_public', publicNames: '_publicNames' @@ -55,6 +60,10 @@ const getLocalAuthInfo = () => { return keytar.getPassword(SERVICE, ACCOUNT); }; +const clearLocalAuthInfo = () => { + return keytar.deletePassword(SERVICE, ACCOUNT); +}; + export const hasLocalAuthInfo = () => { return getLocalAuthInfo(); }; @@ -75,13 +84,29 @@ export const authorise = () => { }); }; -export const connect = () => { - const authInfo = JSON.parse(getLocalAuthInfo()); - if (!authInfo.data) { - return Promise.reject(new Error('Improper auth data')); +export const connect = (res) => { + const authInfo = res || JSON.parse(getLocalAuthInfo()); + if (!authInfo) { + return authorise(); } - return safeApp.fromAuthURI(APP_INFO.data, authInfo.data) - .then((app) => (safe = app)); + return safeApp.fromAuthURI(APP_INFO.data, authInfo) + .then((app) => { + if (res) { + saveAuthInfo(res); + } + (safe = app) + }) + .catch((err) => { + if (err[0] === AUTH_RES_TYPES.containers) { + return Promise.resolve(AUTH_RES_TYPES.containers); + } else if (err[0] === AUTH_RES_TYPES.revoked) { + clearLocalAuthInfo(); + return Promise.resolve(AUTH_RES_TYPES.revoked); + } else { + clearLocalAuthInfo(); + return Promise.reject(err); + } + }); }; export const fetchAccessInfo = () => { @@ -99,7 +124,7 @@ export const fetchAccessInfo = () => { } }) .catch((e) => { - keytar.deletePassword(SERVICE, ACCOUNT); + clearLocalAuthInfo(); return Promise.reject(e); }); }; @@ -452,7 +477,7 @@ export const checkServiceExist = (publicId, service, path) => { }); }) .catch((err) => { - if (err.name === 'ERR_NO_SUCH_ENTRY') { + if (err.code === -106) { return Promise.resolve(false); } return Promise.reject(); diff --git a/web_hosting_manager/app/locales/en.js b/web_hosting_manager/app/locales/en.js index 54eae44..010c1fd 100644 --- a/web_hosting_manager/app/locales/en.js +++ b/web_hosting_manager/app/locales/en.js @@ -19,7 +19,8 @@ const en = { publicContainers: 'Fetching _public Container', preparingApp: 'Preparing Application', connectionErrorTitle: 'Failed To Connect', - authErrorTitle: 'Application Initialisation Failed' + authErrorTitle: 'Application Initialisation Failed', + revoked: 'Application Revoked' }, networkStatus: { connecting: 'connecting', diff --git a/web_hosting_manager/app/reducers/auth.js b/web_hosting_manager/app/reducers/auth.js index 5782bcb..1eeca96 100644 --- a/web_hosting_manager/app/reducers/auth.js +++ b/web_hosting_manager/app/reducers/auth.js @@ -6,7 +6,8 @@ import { I18n } from 'react-redux-i18n'; const initialState = { isAuthorised: false, isAuthorising: false, - error: null + error: null, + isRevoked: false }; const auth = (state: Object = initialState, action: Object) => { @@ -21,7 +22,8 @@ const auth = (state: Object = initialState, action: Object) => { case Action.AUTH_REQUEST_SENT: state = { ...state, - isAuthorising: true + isAuthorising: true, + isRevoked: false }; break; @@ -48,6 +50,12 @@ const auth = (state: Object = initialState, action: Object) => { error: action.payload.message || I18n.t('messages.authorisationFailed') }; break; + case Action.REVOKED: + state = { + ...state, + isRevoked: true + } + break; } return state; }; diff --git a/web_hosting_manager/app/styles/ant_customisation.css b/web_hosting_manager/app/styles/ant_customisation.css index 572fcec..668dc52 100644 --- a/web_hosting_manager/app/styles/ant_customisation.css +++ b/web_hosting_manager/app/styles/ant_customisation.css @@ -13,3 +13,7 @@ .ant-popover-arrow { margin-left: -5px !important; } + +.ant-input-group-wrapper { + width: 100%; +} diff --git a/web_hosting_manager/app/styles/app.css b/web_hosting_manager/app/styles/app.css index 437d1a6..d53c786 100644 --- a/web_hosting_manager/app/styles/app.css +++ b/web_hosting_manager/app/styles/app.css @@ -169,6 +169,10 @@ nav { margin-bottom: 40px; } +.create-service .create-service-input .ant-input-group-wrapper { + width: 100% !important; +} + .split-cntr { position: relative; } @@ -183,6 +187,7 @@ nav { width: 50%; display: inline-block; float: left; + text-align: center; } .split-cntr-i:first-child { @@ -274,6 +279,10 @@ nav { margin-bottom: 16px; } +.custom-card .ant-card-body .ant-input-group-wrapper { + width: 60%; +} + .custom-card .ant-card-body .ant-input-wrapper { margin-bottom: 16px; } diff --git a/web_hosting_manager/package.json b/web_hosting_manager/package.json index 243edc7..c033531 100644 --- a/web_hosting_manager/package.json +++ b/web_hosting_manager/package.json @@ -152,7 +152,7 @@ "webpack-validator": "^2.2.9" }, "dependencies": { - "antd": "^2.5.0", + "antd": "2.5.0", "electron-debug": "^1.0.1", "font-awesome": "^4.7.0", "json-loader": "^0.5.4", From d4002a13d4c45d532ec8396f179d2d615d7ed25e Mon Sep 17 00:00:00 2001 From: Shankar Date: Thu, 15 Jun 2017 14:53:27 +0530 Subject: [PATCH 28/44] fix/tagtype_issue: resolve tagtype issue (#188) Resolve tagtype issue of service Data. File update after deleted resolved. Error description UI issue fixed. --- web_hosting_manager/app/lib/Downloader.js | 4 ++-- web_hosting_manager/app/lib/api.js | 24 ++++++++++++---------- web_hosting_manager/app/lib/tasks.js | 6 +++--- web_hosting_manager/app/utils/app_utils.js | 2 +- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/web_hosting_manager/app/lib/Downloader.js b/web_hosting_manager/app/lib/Downloader.js index c79ccc5..4914d41 100644 --- a/web_hosting_manager/app/lib/Downloader.js +++ b/web_hosting_manager/app/lib/Downloader.js @@ -2,7 +2,7 @@ import fs from 'fs'; import path from 'path'; import { getPath } from './temp'; import { shell } from 'electron'; -import { safe, typetag, accessContainers } from './api'; +import { safe, TAG_TYPE_WWW, accessContainers } from './api'; export default class Downloader { constructor(networkPath, callback) { @@ -21,7 +21,7 @@ export default class Downloader { return safe.auth.getContainer(accessContainers.public) .then((mdata) => mdata.encryptKey(containerPath.dir).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf))) - .then((val) => safe.mutableData.newPublic(val, typetag)) + .then((val) => safe.mutableData.newPublic(val, TAG_TYPE_WWW)) .then((mdata) => { const nfs = mdata.emulateAs('NFS'); return nfs.fetch(containerPath.file) diff --git a/web_hosting_manager/app/lib/api.js b/web_hosting_manager/app/lib/api.js index bed83dd..4a59b18 100644 --- a/web_hosting_manager/app/lib/api.js +++ b/web_hosting_manager/app/lib/api.js @@ -48,7 +48,9 @@ export const accessContainers = { publicNames: '_publicNames' }; -export const typetag = 15001; +export const TAG_TYPE_DNS = 15001; +export const TAG_TYPE_WWW = 15002; + let publicIds = {}; let uploader; @@ -164,7 +166,7 @@ export const fetchServices = () => { return safe.auth.getContainer(accessContainers.publicNames) .then((mdata) => mdata.encryptKey(publicId).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf))) .then((decVal) => { - return safe.mutableData.newPublic(decVal, typetag) + return safe.mutableData.newPublic(decVal, TAG_TYPE_DNS) }) .then((mut) => mut.getEntries() .then((entries) => entries.forEach((key, val) => { @@ -196,7 +198,7 @@ export const createPublicId = (publicId) => { let publicIdName = null; return safe.crypto.sha3Hash(publicId) - .then((hashVal) => safe.mutableData.newPublic(hashVal, typetag)) + .then((hashVal) => safe.mutableData.newPublic(hashVal, TAG_TYPE_DNS)) .then((mdata) => { let permissionSet = null; let permissions = null; @@ -243,7 +245,7 @@ export const createService = (publicId, service, container) => { return mdata.encryptKey(publicId).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf)); }) .then((val) => { - return safe.mutableData.newPublic(val, typetag); + return safe.mutableData.newPublic(val, TAG_TYPE_DNS); }) .then((publicIdMData) => { return publicIdMData.getEntries() @@ -259,7 +261,7 @@ export const createService = (publicId, service, container) => { export const deleteService = (publicId, service) => { return safe.crypto.sha3Hash(publicId) - .then((hashVal) => safe.mutableData.newPublic(hashVal, typetag)) + .then((hashVal) => safe.mutableData.newPublic(hashVal, TAG_TYPE_DNS)) .then((mdata) => mdata.getEntries() .then((entries) => entries.get(service) .then((val) => entries.mutate() @@ -268,7 +270,7 @@ export const deleteService = (publicId, service) => { }; export const createContainer = (path) => { - return safe.mutableData.newRandomPublic(typetag) + return safe.mutableData.newRandomPublic(TAG_TYPE_WWW) .then((mdata) => { return mdata.quickSetup({}) .then(() => { @@ -327,7 +329,7 @@ export const deleteItem = (nwPath) => { if (fileName) { return mdata.encryptKey(dirName).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf)) .then((val) => { - return safe.mutableData.newPublic(val, typetag) + return safe.mutableData.newPublic(val, TAG_TYPE_WWW) .then((dirMdata) => dirMdata.getEntries() .then(() => dirMdata.getEntries() .then((dirEntries) => dirEntries.get(fileName) @@ -338,7 +340,7 @@ export const deleteItem = (nwPath) => { } else { return mdata.encryptKey(nwPath.split('/').slice(0, -1).join('/')).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf)) .then((val) => { - return safe.mutableData.newPublic(val, typetag) + return safe.mutableData.newPublic(val, TAG_TYPE_WWW) .then((tarMdata) => { const targetKeys = []; return tarMdata.getEntries() @@ -372,7 +374,7 @@ export const remapService = (service, publicId, container) => { .then(() => safe.auth.getContainer(accessContainers.publicNames)) .then((mdata) => mdata.encryptKey(publicId).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf))) // .then((entries) => entries.get(publicId)) - .then((val) => safe.mutableData.newPublic(val, typetag)) + .then((val) => safe.mutableData.newPublic(val, TAG_TYPE_DNS)) .then((publicIdMData) => publicIdMData.getEntries() .then(() => publicIdMData.getEntries() .then((entries) => entries.get(service) @@ -387,7 +389,7 @@ export const getContainer = (path) => { .then((mdata) => mdata.encryptKey(path.split('/').slice(0, 3).join('/')) .then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf))) // .then((entries) => entries.get(path.split('/').slice(0, 3).join('/'))) - .then((val) => safe.mutableData.newPublic(val, typetag)) + .then((val) => safe.mutableData.newPublic(val, TAG_TYPE_WWW)) .then((mdata) => { const files = []; const nfs = mdata.emulateAs('NFS'); @@ -455,7 +457,7 @@ export const checkServiceExist = (publicId, service, path) => { return safe.auth.getContainer(accessContainers.publicNames) .then((mdata) => mdata.encryptKey(publicId).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf))) .then((decVal) => { - return safe.mutableData.newPublic(decVal, typetag) + return safe.mutableData.newPublic(decVal, TAG_TYPE_DNS) }) .then((pubMut) => { return pubMut.get(service) diff --git a/web_hosting_manager/app/lib/tasks.js b/web_hosting_manager/app/lib/tasks.js index da806ee..b2ec813 100644 --- a/web_hosting_manager/app/lib/tasks.js +++ b/web_hosting_manager/app/lib/tasks.js @@ -1,7 +1,7 @@ import path from 'path'; import fs from 'fs'; import { I18n } from 'react-redux-i18n'; -import { safe, typetag, accessContainers } from './api'; +import { safe, TAG_TYPE_WWW, accessContainers } from './api'; const parseContainerPath = (targetPath) => { if (!targetPath) { @@ -38,13 +38,13 @@ export class FileUploadTask extends Task { return safe.auth.getContainer(accessContainers.public) .then((mdata) => mdata.encryptKey(containerPath.target).then((encKey) => mdata.get(encKey)).then((value) => mdata.decrypt(value.buf))) - .then((val) => safe.mutableData.newPublic(val, typetag)) + .then((val) => safe.mutableData.newPublic(val, TAG_TYPE_WWW)) .then((mdata) => { const nfs = mdata.emulateAs('NFS'); return nfs.create(fs.readFileSync(this.localPath)) .then((file) => nfs.insert(containerPath.file, file) .catch((err) => { - if (err.name !== 'ERR_ENTRY_EXISTS') { + if (err.code === -106) { return Promise.reject(err); } return mdata.get(containerPath.file) diff --git a/web_hosting_manager/app/utils/app_utils.js b/web_hosting_manager/app/utils/app_utils.js index f8c5fca..e9d62b8 100644 --- a/web_hosting_manager/app/utils/app_utils.js +++ b/web_hosting_manager/app/utils/app_utils.js @@ -1,5 +1,5 @@ export const trimErrorMsg = (msg) => { let index = msg.indexOf('->'); - index = (index === -1) ? index : index + 2; + index = (index === -1) ? 0 : index + 2; return msg.slice(index).trim() }; From 78d1214a005af146d5db9ae3d9da499a6b9ab0f3 Mon Sep 17 00:00:00 2001 From: Gabriel Viganotti Date: Mon, 19 Jun 2017 19:31:33 +0900 Subject: [PATCH 29/44] fix/Removing warnings and errors reported by React (#190) * fix/remove warning messages about deprecated way of usage for React PropTypes * fix/Remove errors returned by React about bindActionCreators receiving incorrect parameters --- .../app/actions/actionTypes.js | 35 ++++++++ web_hosting_manager/app/actions/app.js | 86 ++++++------------- web_hosting_manager/app/components/Auth.js | 67 ++++++++------- .../app/components/CreateService.js | 35 ++++---- .../app/components/FileExplorer.js | 50 +++++------ web_hosting_manager/app/components/Home.js | 57 ++++++------ web_hosting_manager/app/components/Nav.js | 11 +-- .../app/components/NetworkStatus.js | 13 +-- web_hosting_manager/app/containers/App.js | 11 +-- .../app/reducers/access_info.js | 10 +-- web_hosting_manager/app/reducers/auth.js | 14 +-- .../app/reducers/connection.js | 10 +-- .../app/reducers/containers.js | 26 +++--- web_hosting_manager/app/reducers/file.js | 22 ++--- web_hosting_manager/app/reducers/public_id.js | 24 +++--- web_hosting_manager/app/reducers/service.js | 30 +++---- web_hosting_manager/package.json | 15 ++-- 17 files changed, 265 insertions(+), 251 deletions(-) create mode 100644 web_hosting_manager/app/actions/actionTypes.js diff --git a/web_hosting_manager/app/actions/actionTypes.js b/web_hosting_manager/app/actions/actionTypes.js new file mode 100644 index 0000000..8cdcb3f --- /dev/null +++ b/web_hosting_manager/app/actions/actionTypes.js @@ -0,0 +1,35 @@ +const ACTION_TYPES = { + RESET: 'RESET', + AUTH_REQUEST_SENT: 'AUTH_REQUEST_SENT', + AUTH_REQUEST_SEND_FAILED: 'AUTH_REQUEST_SEND_FAILED', + CONNECT: 'CONNECT', + ON_AUTH_SUCCESS: 'ON_AUTH_SUCCESS', + ON_AUTH_FAILURE: 'ON_AUTH_FAILURE', + + CREATE_PUBLIC_ID: 'CREATE_PUBLIC_ID', + CREATE_SERVICE: 'CREATE_SERVICE', + DELETE_SERVICE: 'DELETE_SERVICE', + REMAP_SERVICE: 'REMAP_SERVICE', + CREATE_CONTAINER_AND_SERVICE: 'CREATE_CONTAINER_AND_SERVICE', + FETCH_ACCESS_INFO: 'FETCH_ACCESS_INFO', + FETCH_PUBLIC_NAMES: 'FETCH_PUBLIC_NAMES', + FETCH_SERVICES: 'FETCH_SERVICES', + FETCH_PUBLIC_CONTAINERS: 'FETCH_PUBLIC_CONTAINERS', + FETCH_CONTAINER: 'FETCH_CONTAINER', + + UPLOAD_STARTED: 'UPLOAD_STARTED', + UPLOADING: 'UPLOADING', + UPLOAD_FAILED: 'UPLOAD_FAILED', + UPLOAD_COMPLETED: 'UPLOAD_COMPLETED', + + DOWNLOAD_STARTED: 'DOWNLOAD_STARTED', + DOWNLOADING: 'DOWNLOADING', + DOWNLOAD_FAILED: 'DOWNLOAD_FAILED', + DOWNLOAD_COMPLETED: 'DOWNLOAD_COMPLETED', + CLEAR_NOTIFICATION: 'CLEAR_NOTIFICATION', + REVOKED: 'REVOKED', + + DELETE: 'DELETE' +}; + +export default ACTION_TYPES; diff --git a/web_hosting_manager/app/actions/app.js b/web_hosting_manager/app/actions/app.js index faf9571..8c0aa1c 100644 --- a/web_hosting_manager/app/actions/app.js +++ b/web_hosting_manager/app/actions/app.js @@ -2,41 +2,10 @@ import * as api from '../lib/api'; import { I18n } from 'react-redux-i18n'; - -export const RESET = 'RESET'; -export const AUTH_REQUEST_SENT = 'AUTH_REQUEST_SENT'; -export const AUTH_REQUEST_SEND_FAILED = 'AUTH_REQUEST_SEND_FAILED'; -export const CONNECT = 'CONNECT'; -export const ON_AUTH_SUCCESS = 'ON_AUTH_SUCCESS'; -export const ON_AUTH_FAILURE = 'ON_AUTH_FAILURE'; - -export const CREATE_PUBLIC_ID = 'CREATE_PUBLIC_ID'; -export const CREATE_SERVICE = 'CREATE_SERVICE'; -export const DELETE_SERVICE = 'DELETE_SERVICE'; -export const REMAP_SERVICE = 'REMAP_SERVICE'; -export const CREATE_CONTAINER_AND_SERVICE = 'CREATE_CONTAINER_AND_SERVICE'; -export const FETCH_ACCESS_INFO = 'FETCH_ACCESS_INFO'; -export const FETCH_PUBLIC_NAMES = 'FETCH_PUBLIC_NAMES'; -export const FETCH_SERVICES = 'FETCH_SERVICES'; -export const FETCH_PUBLIC_CONTAINERS = 'FETCH_PUBLIC_CONTAINERS'; -export const FETCH_CONTAINER = 'FETCH_CONTAINER'; - -export const UPLOAD_STARTED = 'UPLOAD_STARTED'; -export const UPLOADING = 'UPLOADING'; -export const UPLOAD_FAILED = 'UPLOAD_FAILED'; -export const UPLOAD_COMPLETED = 'UPLOAD_COMPLETED'; - -export const DOWNLOAD_STARTED = 'DOWNLOAD_STARTED'; -export const DOWNLOADING = 'DOWNLOADING'; -export const DOWNLOAD_FAILED = 'DOWNLOAD_FAILED'; -export const DOWNLOAD_COMPLETED = 'DOWNLOAD_COMPLETED'; -export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION'; -export const REVOKED = 'REVOKED'; - -export const DELETE = 'DELETE'; +import ACTION_TYPES from './actionTypes'; const sendAuthRequest = () => { - const action = api.authorise() ? AUTH_REQUEST_SENT : AUTH_REQUEST_SEND_FAILED; + const action = api.authorise() ? ACTION_TYPES.AUTH_REQUEST_SENT : ACTION_TYPES.AUTH_REQUEST_SEND_FAILED; return { type: action }; @@ -44,13 +13,13 @@ const sendAuthRequest = () => { export const reset = () => { return { - type: RESET + type: ACTION_TYPES.RESET }; }; export const revoked = () => { return { - type: REVOKED + type: ACTION_TYPES.REVOKED }; }; @@ -60,7 +29,7 @@ export const connect = (authRes: String) => { } return (dispatch) => { return dispatch({ - type: CONNECT, + type: ACTION_TYPES.CONNECT, payload: api.connect(authRes) .then((resType) => { if (resType === api.AUTH_RES_TYPES.revoked) { @@ -73,34 +42,34 @@ export const connect = (authRes: String) => { export const onAuthSuccess = (authInfo: Object) => { return { - type: ON_AUTH_SUCCESS + type: ACTION_TYPES.ON_AUTH_SUCCESS }; }; export const onAuthFailure = (error: Object) => { return { - type: ON_AUTH_FAILURE, + type: ACTION_TYPES.ON_AUTH_FAILURE, payload: error }; }; export const getAccessInfo = () => { return { - type: FETCH_ACCESS_INFO, + type: ACTION_TYPES.FETCH_ACCESS_INFO, payload: api.fetchAccessInfo() }; }; export const getPublicNames = () => { return { - type: FETCH_PUBLIC_NAMES, + type: ACTION_TYPES.FETCH_PUBLIC_NAMES, payload: api.fetchPublicNames() }; }; export const createPublicId = (publicId: string) => { return { - type: CREATE_PUBLIC_ID, + type: ACTION_TYPES.CREATE_PUBLIC_ID, payload: api.createPublicId(publicId) .then(() => { return api.fetchPublicNames(publicId); @@ -112,7 +81,7 @@ export const createContainerAndService = (publicId: string, service: string, conatinerName: string, parentConatiner: string) => { const path = `${parentConatiner}/${publicId}/${conatinerName}`; return { - type: CREATE_CONTAINER_AND_SERVICE, + type: ACTION_TYPES.CREATE_CONTAINER_AND_SERVICE, payload: api.checkServiceExist(publicId, service, path) .then((exist) => { if (!exist) { @@ -129,14 +98,14 @@ export const createContainerAndService = (publicId: string, service: string, export const createService = (publicId: string, service: string, containerPath: string) => { return { - type: CREATE_SERVICE, + type: ACTION_TYPES.CREATE_SERVICE, payload: api.createService(publicId, service, containerPath) }; }; export const deleteService = (publicId: string, service: string) => { return { - type: DELETE_SERVICE, + type: ACTION_TYPES.DELETE_SERVICE, payload: api.deleteService(publicId, service) .then(() => api.fetchServices()) }; @@ -144,21 +113,21 @@ export const deleteService = (publicId: string, service: string) => { export const getServices = () => { return { - type: FETCH_SERVICES, + type: ACTION_TYPES.FETCH_SERVICES, payload: api.fetchServices() }; }; export const getPublicContainers = () => { return { - type: FETCH_PUBLIC_CONTAINERS, + type: ACTION_TYPES.FETCH_PUBLIC_CONTAINERS, payload: api.getPublicContainers() }; }; export const remapService = (service: string, publicId: string, containerPath: string) => { return { - type: REMAP_SERVICE, + type: ACTION_TYPES.REMAP_SERVICE, payload: api.remapService(service, publicId, containerPath) .then(() => api.fetchServices()) }; @@ -166,7 +135,7 @@ export const remapService = (service: string, publicId: string, containerPath: s export const getContainer = (containerPath: string) => { return { - type: FETCH_CONTAINER, + type: ACTION_TYPES.FETCH_CONTAINER, payload: api.getContainer(containerPath) }; }; @@ -175,7 +144,7 @@ export const upload = (localPath: string, networkPath: string) => { return (dispatch) => { const progressCallback = (status, isCompleted) => { dispatch({ - type: isCompleted ? UPLOAD_COMPLETED : UPLOADING, + type: isCompleted ? ACTION_TYPES.UPLOAD_COMPLETED : ACTION_TYPES.UPLOADING, payload: status }); if (isCompleted) { @@ -184,13 +153,13 @@ export const upload = (localPath: string, networkPath: string) => { }; const errorCallback = (error) => { dispatch({ - type: UPLOAD_FAILED, + type: ACTION_TYPES.UPLOAD_FAILED, payload: error }); }; api.upload(localPath, networkPath, progressCallback, errorCallback); dispatch({ - type: UPLOAD_STARTED + type: ACTION_TYPES.UPLOAD_STARTED }); }; }; @@ -199,7 +168,7 @@ export const cancelUpload = () => { api.cancelUpload(); const err = new Error(I18n.t('messages.uploadCancelled')); return { - type: UPLOAD_FAILED, + type: ACTION_TYPES.UPLOAD_FAILED, payload: err }; }; @@ -207,17 +176,17 @@ export const cancelUpload = () => { export const download = (networkPath: string) => { return (dispatch) => { dispatch({ - type: DOWNLOAD_STARTED + type: ACTION_TYPES.DOWNLOAD_STARTED }); api.download(networkPath, (err, status) => { if (err) { return dispatch({ - type: DOWNLOAD_FAILED, + type: ACTION_TYPES.DOWNLOAD_FAILED, payload: err }); } dispatch({ - type: status.completed ? DOWNLOAD_COMPLETED : DOWNLOADING, + type: status.completed ? ACTION_TYPES.DOWNLOAD_COMPLETED : ACTION_TYPES.DOWNLOADING, payload: status.progress }); }); @@ -228,15 +197,14 @@ export const cancelDownload = () => { api.cancelDownload(); const err = new Error(I18n.t('messages.downloadCancelled')); return { - type: DOWNLOAD_FAILED, + type: ACTION_TYPES.DOWNLOAD_FAILED, payload: err }; }; - export const deleteItem = (containerPath, name) => { return { - type: DELETE, + type: ACTION_TYPES.DELETE, payload: api.deleteItem(`${containerPath}/${name}`) .then(() => { return api.getContainer(containerPath); @@ -246,6 +214,6 @@ export const deleteItem = (containerPath, name) => { export const clearNotification = () => { return { - type: CLEAR_NOTIFICATION + type: ACTION_TYPES.CLEAR_NOTIFICATION } }; diff --git a/web_hosting_manager/app/components/Auth.js b/web_hosting_manager/app/components/Auth.js index 049e410..2a153c1 100644 --- a/web_hosting_manager/app/components/Auth.js +++ b/web_hosting_manager/app/components/Auth.js @@ -1,40 +1,9 @@ import { Button, Card, Icon } from 'antd'; -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import { I18n } from 'react-redux-i18n'; export default class Auth extends Component { - static propTypes = { - connect: PropTypes.func.isRequired, - getAccessInfo: PropTypes.func.isRequired, - getPublicNames: PropTypes.func.isRequired, - getServices: PropTypes.func.isRequired, - reset: PropTypes.func.isRequired, - - authError: PropTypes.string, - isAuthorising: PropTypes.bool.isRequired, - isAuthorised: PropTypes.bool.isRequired, - - isConnecting: PropTypes.bool.isRequired, - isConnected: PropTypes.bool.isRequired, - connectionError: PropTypes.string, - - fetchingAccessInfo: PropTypes.bool.isRequired, - fetchedAccessInfo: PropTypes.bool.isRequired, - accessInfoError: PropTypes.string, - - fetchingPublicNames: PropTypes.bool.isRequired, - fetchedPublicNames: PropTypes.bool.isRequired, - publicNameError: PropTypes.string, - - fetchingServices: PropTypes.bool.isRequired, - fetchedServices: PropTypes.bool.isRequired, - serviceError: PropTypes.string, - - fetchingPublicContainers: PropTypes.bool.isRequired, - fetchedPublicContainers: PropTypes.bool.isRequired, - publicContainersError: PropTypes.string, - }; - componentDidMount() { if (!this.props.isRevoked) { this.props.connect(); @@ -176,3 +145,35 @@ export default class Auth extends Component { ); } } + +Auth.propTypes = { + connect: PropTypes.func.isRequired, + getAccessInfo: PropTypes.func.isRequired, + getPublicNames: PropTypes.func.isRequired, + getServices: PropTypes.func.isRequired, + reset: PropTypes.func.isRequired, + + authError: PropTypes.string, + isAuthorising: PropTypes.bool.isRequired, + isAuthorised: PropTypes.bool.isRequired, + + isConnecting: PropTypes.bool.isRequired, + isConnected: PropTypes.bool.isRequired, + connectionError: PropTypes.string, + + fetchingAccessInfo: PropTypes.bool.isRequired, + fetchedAccessInfo: PropTypes.bool.isRequired, + accessInfoError: PropTypes.string, + + fetchingPublicNames: PropTypes.bool.isRequired, + fetchedPublicNames: PropTypes.bool.isRequired, + publicNameError: PropTypes.string, + + fetchingServices: PropTypes.bool.isRequired, + fetchedServices: PropTypes.bool.isRequired, + serviceError: PropTypes.string, + + fetchingPublicContainers: PropTypes.bool.isRequired, + fetchedPublicContainers: PropTypes.bool.isRequired, + publicContainersError: PropTypes.string, +}; diff --git a/web_hosting_manager/app/components/CreateService.js b/web_hosting_manager/app/components/CreateService.js index 4e733d5..8321459 100644 --- a/web_hosting_manager/app/components/CreateService.js +++ b/web_hosting_manager/app/components/CreateService.js @@ -1,26 +1,11 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import { Button, Card, Input, Select, Icon } from 'antd'; import { I18n } from 'react-redux-i18n'; import Nav from './Nav'; export default class CreateService extends Component { - static propTypes = { - isConnecting: PropTypes.bool.isRequired, - isConnected: PropTypes.bool.isRequired, - connectionError: PropTypes.string, - creatingService: PropTypes.bool.isRequired, - serviceError: PropTypes.string, - fetchingPublicContainers: PropTypes.bool.isRequired, - publicContainers: PropTypes.array.isRequired, - publicContainersError: PropTypes.string, - params: PropTypes.object.isRequired, - router: PropTypes.object.isRequired, - createContainerAndService: PropTypes.func.isRequired, - createService: PropTypes.func.isRequired, - getPublicContainers: PropTypes.func.isRequired, - } - constructor(props) { super(props); this.containerName = ''; @@ -192,3 +177,19 @@ export default class CreateService extends Component { ); } } + +CreateService.propTypes = { + isConnecting: PropTypes.bool.isRequired, + isConnected: PropTypes.bool.isRequired, + connectionError: PropTypes.string, + creatingService: PropTypes.bool.isRequired, + serviceError: PropTypes.string, + fetchingPublicContainers: PropTypes.bool.isRequired, + publicContainers: PropTypes.array.isRequired, + publicContainersError: PropTypes.string, + params: PropTypes.object.isRequired, + router: PropTypes.object.isRequired, + createContainerAndService: PropTypes.func.isRequired, + createService: PropTypes.func.isRequired, + getPublicContainers: PropTypes.func.isRequired, +} diff --git a/web_hosting_manager/app/components/FileExplorer.js b/web_hosting_manager/app/components/FileExplorer.js index 2e97681..3909765 100644 --- a/web_hosting_manager/app/components/FileExplorer.js +++ b/web_hosting_manager/app/components/FileExplorer.js @@ -1,35 +1,12 @@ import { remote } from 'electron'; import { I18n } from 'react-redux-i18n'; -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import { Card, Button, Icon, Row, Col, notification, Popover, Progress } from 'antd'; import Nav from './Nav'; export default class FileExplorer extends Component { - - static propTypes = { - getContainer: PropTypes.func.isRequired, - upload: PropTypes.func.isRequired, - cancelUpload: PropTypes.func.isRequired, - download: PropTypes.func.isRequired, - cancelDownload: PropTypes.func.isRequired, - deleteItem: PropTypes.func.isRequired, - isConnecting: PropTypes.bool.isRequired, - isConnected: PropTypes.bool.isRequired, - connectionError: PropTypes.string, - fetchingContainer: PropTypes.bool.isRequired, - deleting: PropTypes.bool.isRequired, - containerInfo: PropTypes.array.isRequired, - containerError: PropTypes.string, - uploading: PropTypes.bool.isRequired, - uploadStatus: PropTypes.object, - downloading: PropTypes.bool.isRequired, - downloadProgress: PropTypes.number.isRequired, - fileError: PropTypes.string, - params: PropTypes.object.isRequired, - router: PropTypes.object.isRequired, - } - constructor(props) { super(props); this.containerPath = undefined; @@ -246,3 +223,26 @@ export default class FileExplorer extends Component { ); } } + +FileExplorer.propTypes = { + getContainer: PropTypes.func.isRequired, + upload: PropTypes.func.isRequired, + cancelUpload: PropTypes.func.isRequired, + download: PropTypes.func.isRequired, + cancelDownload: PropTypes.func.isRequired, + deleteItem: PropTypes.func.isRequired, + isConnecting: PropTypes.bool.isRequired, + isConnected: PropTypes.bool.isRequired, + connectionError: PropTypes.string, + fetchingContainer: PropTypes.bool.isRequired, + deleting: PropTypes.bool.isRequired, + containerInfo: PropTypes.array.isRequired, + containerError: PropTypes.string, + uploading: PropTypes.bool.isRequired, + uploadStatus: PropTypes.object, + downloading: PropTypes.bool.isRequired, + downloadProgress: PropTypes.number.isRequired, + fileError: PropTypes.string, + params: PropTypes.object.isRequired, + router: PropTypes.object.isRequired, +} diff --git a/web_hosting_manager/app/components/Home.js b/web_hosting_manager/app/components/Home.js index ff53954..48e1c9e 100644 --- a/web_hosting_manager/app/components/Home.js +++ b/web_hosting_manager/app/components/Home.js @@ -1,5 +1,6 @@ import { Button, Card, Collapse, Input, Modal, Row, Col, Select, Icon } from 'antd'; -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import { I18n } from 'react-redux-i18n'; import { Link } from 'react-router'; @@ -8,33 +9,6 @@ import NetworkStatus from './NetworkStatus'; import Nav from './Nav'; export default class Auth extends Component { - static propTypes = { - connect: PropTypes.func.isRequired, - getAccessInfo: PropTypes.func.isRequired, - getPublicNames: PropTypes.func.isRequired, - getPublicContainers: PropTypes.func.isRequired, - createPublicId: PropTypes.func.isRequired, - remapService: PropTypes.func.isRequired, - - isConnecting: PropTypes.bool.isRequired, - isConnected: PropTypes.bool.isRequired, - connectionError: PropTypes.string, - - fetchingAccessInfo: PropTypes.bool.isRequired, - fetchedAccessInfo: PropTypes.bool.isRequired, - accessInfoError: PropTypes.string, - - fetchingPublicNames: PropTypes.bool.isRequired, - publicNames: PropTypes.object.isRequired, - creatingPublicId: PropTypes.bool.isRequired, - publicIdError: PropTypes.string, - - remapping: PropTypes.bool.isRequired, - serviceError: PropTypes.string, - - publicContainers: PropTypes.array.isRequired - }; - constructor(props) { super(props); this.state = { @@ -336,3 +310,30 @@ export default class Auth extends Component { ); } } + +Auth.propTypes = { + connect: PropTypes.func.isRequired, + getAccessInfo: PropTypes.func.isRequired, + getPublicNames: PropTypes.func.isRequired, + getPublicContainers: PropTypes.func.isRequired, + createPublicId: PropTypes.func.isRequired, + remapService: PropTypes.func.isRequired, + + isConnecting: PropTypes.bool.isRequired, + isConnected: PropTypes.bool.isRequired, + connectionError: PropTypes.string, + + fetchingAccessInfo: PropTypes.bool.isRequired, + fetchedAccessInfo: PropTypes.bool.isRequired, + accessInfoError: PropTypes.string, + + fetchingPublicNames: PropTypes.bool.isRequired, + publicNames: PropTypes.object.isRequired, + creatingPublicId: PropTypes.bool.isRequired, + publicIdError: PropTypes.string, + + remapping: PropTypes.bool.isRequired, + serviceError: PropTypes.string, + + publicContainers: PropTypes.array.isRequired +}; diff --git a/web_hosting_manager/app/components/Nav.js b/web_hosting_manager/app/components/Nav.js index 51a032b..2f255d4 100644 --- a/web_hosting_manager/app/components/Nav.js +++ b/web_hosting_manager/app/components/Nav.js @@ -1,11 +1,8 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import { Icon } from 'antd'; export default class Nav extends Component { - static propTypes = { - title: PropTypes.string.isRequired - } - backButton() { return (
    @@ -24,3 +21,7 @@ export default class Nav extends Component { ); } } + +Nav.propTypes = { + title: PropTypes.string.isRequired +} diff --git a/web_hosting_manager/app/components/NetworkStatus.js b/web_hosting_manager/app/components/NetworkStatus.js index b4dfefe..bfad7df 100644 --- a/web_hosting_manager/app/components/NetworkStatus.js +++ b/web_hosting_manager/app/components/NetworkStatus.js @@ -1,13 +1,9 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import classNames from 'classnames'; import { I18n } from 'react-redux-i18n'; export default class NetworkStatus extends Component { - static propTypes = { - status: PropTypes.number.isRequired, - message: PropTypes.string - }; - render() { return (
    @@ -25,3 +21,8 @@ export default class NetworkStatus extends Component { ); } } + +NetworkStatus.propTypes = { + status: PropTypes.number.isRequired, + message: PropTypes.string +}; diff --git a/web_hosting_manager/app/containers/App.js b/web_hosting_manager/app/containers/App.js index 065260c..5a08e3b 100644 --- a/web_hosting_manager/app/containers/App.js +++ b/web_hosting_manager/app/containers/App.js @@ -1,11 +1,8 @@ // @flow -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; export default class App extends Component { - static propTypes = { - children: PropTypes.element.isRequired - }; - render() { return (
    @@ -14,3 +11,7 @@ export default class App extends Component { ); } } + +App.propTypes = { + children: PropTypes.element.isRequired +}; diff --git a/web_hosting_manager/app/reducers/access_info.js b/web_hosting_manager/app/reducers/access_info.js index 7eaf76c..569121f 100644 --- a/web_hosting_manager/app/reducers/access_info.js +++ b/web_hosting_manager/app/reducers/access_info.js @@ -1,6 +1,6 @@ // @flow -import * as Action from '../actions/app'; +import ACTION_TYPES from '../actions/actionTypes'; import { I18n } from 'react-redux-i18n'; import { trimErrorMsg } from '../utils/app_utils'; @@ -12,21 +12,21 @@ const initialState = { const accessInfo = (state: Object = initialState, action: Object) => { switch (action.type) { - case Action.RESET: + case ACTION_TYPES.RESET: state = { ...state, ...initialState }; break; - case `${Action.FETCH_ACCESS_INFO}_PENDING`: + case `${ACTION_TYPES.FETCH_ACCESS_INFO}_PENDING`: state = { ...state, fetchingAccessInfo: true }; break; - case `${Action.FETCH_ACCESS_INFO}_FULFILLED`: + case `${ACTION_TYPES.FETCH_ACCESS_INFO}_FULFILLED`: state = { ...state, fetchingAccessInfo: false, @@ -34,7 +34,7 @@ const accessInfo = (state: Object = initialState, action: Object) => { }; break; - case `${Action.FETCH_ACCESS_INFO}_REJECTED`: + case `${ACTION_TYPES.FETCH_ACCESS_INFO}_REJECTED`: state = { ...state, fetchingAccessInfo: false, diff --git a/web_hosting_manager/app/reducers/auth.js b/web_hosting_manager/app/reducers/auth.js index 1eeca96..9796177 100644 --- a/web_hosting_manager/app/reducers/auth.js +++ b/web_hosting_manager/app/reducers/auth.js @@ -1,6 +1,6 @@ // @flow -import * as Action from '../actions/app'; +import ACTION_TYPES from '../actions/actionTypes'; import { I18n } from 'react-redux-i18n'; const initialState = { @@ -12,14 +12,14 @@ const initialState = { const auth = (state: Object = initialState, action: Object) => { switch (action.type) { - case Action.RESET: + case ACTION_TYPES.RESET: state = { ...state, ...initialState }; break; - case Action.AUTH_REQUEST_SENT: + case ACTION_TYPES.AUTH_REQUEST_SENT: state = { ...state, isAuthorising: true, @@ -27,7 +27,7 @@ const auth = (state: Object = initialState, action: Object) => { }; break; - case Action.AUTH_REQUEST_SEND_FAILED: + case ACTION_TYPES.AUTH_REQUEST_SEND_FAILED: state = { ...state, isAuthorising: false, @@ -35,7 +35,7 @@ const auth = (state: Object = initialState, action: Object) => { }; break; - case Action.ON_AUTH_SUCCESS: + case ACTION_TYPES.ON_AUTH_SUCCESS: state = { ...state, isAuthorising: false, @@ -43,14 +43,14 @@ const auth = (state: Object = initialState, action: Object) => { }; break; - case Action.ON_AUTH_FAILURE: + case ACTION_TYPES.ON_AUTH_FAILURE: state = { ...state, isAuthorising: false, error: action.payload.message || I18n.t('messages.authorisationFailed') }; break; - case Action.REVOKED: + case ACTION_TYPES.REVOKED: state = { ...state, isRevoked: true diff --git a/web_hosting_manager/app/reducers/connection.js b/web_hosting_manager/app/reducers/connection.js index 9826dcc..9d54430 100644 --- a/web_hosting_manager/app/reducers/connection.js +++ b/web_hosting_manager/app/reducers/connection.js @@ -1,6 +1,6 @@ // @flow -import { CONNECT, RESET } from '../actions/app'; +import ACTION_TYPES from '../actions/actionTypes'; import { I18n } from 'react-redux-i18n'; const initialState = { @@ -11,21 +11,21 @@ const initialState = { const connection = (state: Object = initialState, action: Object) => { switch (action.type) { - case RESET: + case ACTION_TYPES.RESET: state = { ...state, ...initialState }; break; - case `${CONNECT}_PENDING`: + case `${ACTION_TYPES.CONNECT}_PENDING`: state = { ...state, isConnecting: true }; break; - case `${CONNECT}_FULFILLED`: + case `${ACTION_TYPES.CONNECT}_FULFILLED`: state = { ...state, isConnecting: false, @@ -33,7 +33,7 @@ const connection = (state: Object = initialState, action: Object) => { }; break; - case `${CONNECT}_REJECTED`: + case `${ACTION_TYPES.CONNECT}_REJECTED`: state = { ...state, isConnecting: false, diff --git a/web_hosting_manager/app/reducers/containers.js b/web_hosting_manager/app/reducers/containers.js index 2fad4b0..1e1b3d2 100644 --- a/web_hosting_manager/app/reducers/containers.js +++ b/web_hosting_manager/app/reducers/containers.js @@ -1,4 +1,4 @@ -import * as Action from '../actions/app'; +import ACTION_TYPES from '../actions/actionTypes'; import { I18n } from 'react-redux-i18n'; import { trimErrorMsg } from '../utils/app_utils'; @@ -14,14 +14,14 @@ const initialState = { const containers = (state: Object = initialState, action: Object) => { switch (action.type) { - case Action.RESET: + case ACTION_TYPES.RESET: state = { ...state, ...initialState }; break; - case `${Action.FETCH_PUBLIC_CONTAINERS}_PENDING`: + case `${ACTION_TYPES.FETCH_PUBLIC_CONTAINERS}_PENDING`: state = { ...state, publicContainers: [], @@ -29,7 +29,7 @@ const containers = (state: Object = initialState, action: Object) => { }; break; - case `${Action.FETCH_PUBLIC_CONTAINERS}_FULFILLED`: + case `${ACTION_TYPES.FETCH_PUBLIC_CONTAINERS}_FULFILLED`: state = { ...state, fetchingPublicContainers: false, @@ -38,7 +38,7 @@ const containers = (state: Object = initialState, action: Object) => { }; break; - case `${Action.FETCH_PUBLIC_CONTAINERS}_REJECTED`: + case `${ACTION_TYPES.FETCH_PUBLIC_CONTAINERS}_REJECTED`: state = { ...state, fetchedPublicContainers: false, @@ -46,7 +46,7 @@ const containers = (state: Object = initialState, action: Object) => { }; break; - case `${Action.CREATE_CONTAINER_AND_SERVICE}_FULFILLED`: + case `${ACTION_TYPES.CREATE_CONTAINER_AND_SERVICE}_FULFILLED`: const copy = state.publicContainers.map((name) => { return name; }); // copy.push(action.payload); let key = null; @@ -65,7 +65,7 @@ const containers = (state: Object = initialState, action: Object) => { }; break; - case `${Action.FETCH_CONTAINER}_PENDING`: + case `${ACTION_TYPES.FETCH_CONTAINER}_PENDING`: state = { ...state, containerInfo: [], @@ -73,7 +73,7 @@ const containers = (state: Object = initialState, action: Object) => { }; break; - case `${Action.FETCH_CONTAINER}_FULFILLED`: + case `${ACTION_TYPES.FETCH_CONTAINER}_FULFILLED`: state = { ...state, containerInfo: action.payload, @@ -81,7 +81,7 @@ const containers = (state: Object = initialState, action: Object) => { }; break; - case `${Action.FETCH_CONTAINER}_REJECTED`: + case `${ACTION_TYPES.FETCH_CONTAINER}_REJECTED`: state = { ...state, fetchingContainer: false, @@ -90,14 +90,14 @@ const containers = (state: Object = initialState, action: Object) => { }; break; - case `${Action.DELETE}_PENDING`: + case `${ACTION_TYPES.DELETE}_PENDING`: state = { ...state, deleting: true }; break; - case `${Action.DELETE}_FULFILLED`: + case `${ACTION_TYPES.DELETE}_FULFILLED`: state = { ...state, containerInfo: action.payload, @@ -105,14 +105,14 @@ const containers = (state: Object = initialState, action: Object) => { }; break; - case `${Action.DELETE}_REJECTED`: + case `${ACTION_TYPES.DELETE}_REJECTED`: state = { ...state, deleting: false, error: trimErrorMsg(action.payload.message) }; break; - case Action.CLEAR_NOTIFICATION: + case ACTION_TYPES.CLEAR_NOTIFICATION: state = { ...state, error: undefined diff --git a/web_hosting_manager/app/reducers/file.js b/web_hosting_manager/app/reducers/file.js index 1577a8e..b7f406f 100644 --- a/web_hosting_manager/app/reducers/file.js +++ b/web_hosting_manager/app/reducers/file.js @@ -1,6 +1,6 @@ // @flow -import * as Action from '../actions/app'; +import ACTION_TYPES from '../actions/actionTypes'; const initialState = { uploading: false, @@ -12,14 +12,14 @@ const initialState = { const file = (state: Object = initialState, action: Object) => { switch (action.type) { - case Action.RESET: + case ACTION_TYPES.RESET: state = { ...state, ...initialState }; break; - case Action.UPLOAD_STARTED: + case ACTION_TYPES.UPLOAD_STARTED: state = { ...state, uploading: true, @@ -27,21 +27,21 @@ const file = (state: Object = initialState, action: Object) => { }; break; - case Action.UPLOADING: + case ACTION_TYPES.UPLOADING: state = { ...state, uploadStatus: action.payload }; break; - case Action.UPLOAD_COMPLETED: + case ACTION_TYPES.UPLOAD_COMPLETED: state = { ...state, uploading: false, uploadStatus: undefined }; break; - case Action.UPLOAD_FAILED: + case ACTION_TYPES.UPLOAD_FAILED: state = { ...state, uploading: false, @@ -49,7 +49,7 @@ const file = (state: Object = initialState, action: Object) => { error: action.payload.message }; break; - case Action.DOWNLOAD_STARTED: + case ACTION_TYPES.DOWNLOAD_STARTED: state = { ...state, downloading: true, @@ -57,21 +57,21 @@ const file = (state: Object = initialState, action: Object) => { }; break; - case Action.DOWNLOADING: + case ACTION_TYPES.DOWNLOADING: state = { ...state, downloadProgress: action.payload }; break; - case Action.DOWNLOAD_COMPLETED: + case ACTION_TYPES.DOWNLOAD_COMPLETED: state = { ...state, downloading: false, downloadProgress: 0 }; break; - case Action.DOWNLOAD_FAILED: + case ACTION_TYPES.DOWNLOAD_FAILED: state = { ...state, downloading: false, @@ -79,7 +79,7 @@ const file = (state: Object = initialState, action: Object) => { error: action.payload.message }; break; - case Action.CLEAR_NOTIFICATION: + case ACTION_TYPES.CLEAR_NOTIFICATION: state = { ...state, error: undefined diff --git a/web_hosting_manager/app/reducers/public_id.js b/web_hosting_manager/app/reducers/public_id.js index cec9f34..b6a02f8 100644 --- a/web_hosting_manager/app/reducers/public_id.js +++ b/web_hosting_manager/app/reducers/public_id.js @@ -1,6 +1,6 @@ // @flow -import * as Action from '../actions/app'; +import ACTION_TYPES from '../actions/actionTypes'; import { I18n } from 'react-redux-i18n'; import { trimErrorMsg } from '../utils/app_utils'; @@ -14,14 +14,14 @@ const initialState = { const publicId = (state: Object = initialState, action: Object) => { switch (action.type) { - case Action.RESET: + case ACTION_TYPES.RESET: state = { ...state, ...initialState }; break; - case `${Action.FETCH_PUBLIC_NAMES}_PENDING`: + case `${ACTION_TYPES.FETCH_PUBLIC_NAMES}_PENDING`: state = { ...state, publicNames: {}, @@ -29,7 +29,7 @@ const publicId = (state: Object = initialState, action: Object) => { }; break; - case `${Action.FETCH_PUBLIC_NAMES}_FULFILLED`: + case `${ACTION_TYPES.FETCH_PUBLIC_NAMES}_FULFILLED`: { const publicNames = {}; let pkey = null; @@ -45,7 +45,7 @@ const publicId = (state: Object = initialState, action: Object) => { } break; - case `${Action.FETCH_PUBLIC_NAMES}_REJECTED`: + case `${ACTION_TYPES.FETCH_PUBLIC_NAMES}_REJECTED`: state = { ...state, fetchingPublicNames: false, @@ -54,14 +54,14 @@ const publicId = (state: Object = initialState, action: Object) => { }; break; - case `${Action.CREATE_PUBLIC_ID}_PENDING`: + case `${ACTION_TYPES.CREATE_PUBLIC_ID}_PENDING`: state = { ...state, creatingPublicId: true }; break; - case `${Action.CREATE_PUBLIC_ID}_FULFILLED`: + case `${ACTION_TYPES.CREATE_PUBLIC_ID}_FULFILLED`: { const publicNames = {}; let pkey = null; @@ -77,7 +77,7 @@ const publicId = (state: Object = initialState, action: Object) => { } break; - case `${Action.CREATE_PUBLIC_ID}_REJECTED`: + case `${ACTION_TYPES.CREATE_PUBLIC_ID}_REJECTED`: state = { ...state, creatingPublicId: false, @@ -85,7 +85,7 @@ const publicId = (state: Object = initialState, action: Object) => { }; break; - case `${Action.DELETE_SERVICE}_FULFILLED`: + case `${ACTION_TYPES.DELETE_SERVICE}_FULFILLED`: { const publicNames = {}; let pkey = null; @@ -99,7 +99,7 @@ const publicId = (state: Object = initialState, action: Object) => { } break; - case `${Action.CREATE_CONTAINER_AND_SERVICE}_FULFILLED`: + case `${ACTION_TYPES.CREATE_CONTAINER_AND_SERVICE}_FULFILLED`: { const publicNames = {}; let pkey = null; @@ -113,7 +113,7 @@ const publicId = (state: Object = initialState, action: Object) => { } break; - case `${Action.FETCH_SERVICES}_FULFILLED`: + case `${ACTION_TYPES.FETCH_SERVICES}_FULFILLED`: { const publicNames = {}; let pkey = null; @@ -127,7 +127,7 @@ const publicId = (state: Object = initialState, action: Object) => { } break; - case `${Action.REMAP_SERVICE}_FULFILLED`: + case `${ACTION_TYPES.REMAP_SERVICE}_FULFILLED`: const publicNames = {}; let pkey = null; for (pkey of Object.keys(action.payload)) { diff --git a/web_hosting_manager/app/reducers/service.js b/web_hosting_manager/app/reducers/service.js index eaa045b..f175418 100644 --- a/web_hosting_manager/app/reducers/service.js +++ b/web_hosting_manager/app/reducers/service.js @@ -1,6 +1,6 @@ // @flow -import * as Action from '../actions/app'; +import ACTION_TYPES from '../actions/actionTypes'; import { I18n } from 'react-redux-i18n'; import { trimErrorMsg } from '../utils/app_utils'; @@ -14,21 +14,21 @@ const initialState = { const service = (state: Object = initialState, action: Object) => { switch (action.type) { - case Action.RESET: + case ACTION_TYPES.RESET: state = { ...state, ...initialState }; break; - case `${Action.FETCH_SERVICES}_PENDING`: + case `${ACTION_TYPES.FETCH_SERVICES}_PENDING`: state = { ...state, fetchingServices: true }; break; - case `${Action.FETCH_SERVICES}_FULFILLED`: + case `${ACTION_TYPES.FETCH_SERVICES}_FULFILLED`: state = { ...state, fetchingServices: false, @@ -37,7 +37,7 @@ const service = (state: Object = initialState, action: Object) => { }; break; - case `${Action.FETCH_SERVICES}_REJECTED`: + case `${ACTION_TYPES.FETCH_SERVICES}_REJECTED`: state = { ...state, fetchingServices: false, @@ -45,7 +45,7 @@ const service = (state: Object = initialState, action: Object) => { }; break; - case `${Action.CREATE_SERVICE}_PENDING`: + case `${ACTION_TYPES.CREATE_SERVICE}_PENDING`: state = { ...state, creatingService: true, @@ -53,14 +53,14 @@ const service = (state: Object = initialState, action: Object) => { }; break; - case `${Action.CREATE_SERVICE}_FULFILLED`: + case `${ACTION_TYPES.CREATE_SERVICE}_FULFILLED`: state = { ...state, creatingService: false, }; break; - case `${Action.CREATE_SERVICE}_REJECTED`: + case `${ACTION_TYPES.CREATE_SERVICE}_REJECTED`: state = { ...state, creatingService: false, @@ -68,7 +68,7 @@ const service = (state: Object = initialState, action: Object) => { }; break; - case `${Action.CREATE_CONTAINER_AND_SERVICE}_PENDING`: + case `${ACTION_TYPES.CREATE_CONTAINER_AND_SERVICE}_PENDING`: state = { ...state, creatingService: true, @@ -76,7 +76,7 @@ const service = (state: Object = initialState, action: Object) => { }; break; - case `${Action.CREATE_CONTAINER_AND_SERVICE}_FULFILLED`: + case `${ACTION_TYPES.CREATE_CONTAINER_AND_SERVICE}_FULFILLED`: state = { ...state, creatingService: false, @@ -84,7 +84,7 @@ const service = (state: Object = initialState, action: Object) => { }; break; - case `${Action.CREATE_CONTAINER_AND_SERVICE}_REJECTED`: + case `${ACTION_TYPES.CREATE_CONTAINER_AND_SERVICE}_REJECTED`: state = { ...state, creatingService: false, @@ -92,7 +92,7 @@ const service = (state: Object = initialState, action: Object) => { }; break; - case `${Action.REMAP_SERVICE}_PENDING`: + case `${ACTION_TYPES.REMAP_SERVICE}_PENDING`: state = { ...state, remapping: true, @@ -100,7 +100,7 @@ const service = (state: Object = initialState, action: Object) => { }; break; - case `${Action.REMAP_SERVICE}_FULFILLED`: + case `${ACTION_TYPES.REMAP_SERVICE}_FULFILLED`: state = { ...state, remapping: false, @@ -108,14 +108,14 @@ const service = (state: Object = initialState, action: Object) => { }; break; - case `${Action.REMAP_SERVICE}_REJECTED`: + case `${ACTION_TYPES.REMAP_SERVICE}_REJECTED`: state = { ...state, remapping: false, error: trimErrorMsg(action.payload.message) }; break; - case Action.CLEAR_NOTIFICATION: + case ACTION_TYPES.CLEAR_NOTIFICATION: state = { ...state, error: undefined diff --git a/web_hosting_manager/package.json b/web_hosting_manager/package.json index c033531..6ae46cf 100644 --- a/web_hosting_manager/package.json +++ b/web_hosting_manager/package.json @@ -57,11 +57,15 @@ "linux": { "target": "dir" }, - "protocols": [{ - "name": "safehostingmanager", - "role": "Viewer", - "schemes": ["safe-bmv0lm1hawrzywzllnnhzmvob3n0aw5nbwfuywdlcg"] - }] + "protocols": [ + { + "name": "safehostingmanager", + "role": "Viewer", + "schemes": [ + "safe-bmv0lm1hawrzywzllnnhzmvob3n0aw5nbwfuywdlcg" + ] + } + ] }, "directories": { "buildResources": "resources", @@ -156,6 +160,7 @@ "electron-debug": "^1.0.1", "font-awesome": "^4.7.0", "json-loader": "^0.5.4", + "prop-types": "^15.5.10", "react": "^15.3.2", "react-dom": "^15.3.2", "react-redux": "^4.4.5", From 4db2d2a67c5f3d3894ac7160d331c3759cd302aa Mon Sep 17 00:00:00 2001 From: Gabriel Viganotti Date: Wed, 21 Jun 2017 13:22:11 +0900 Subject: [PATCH 30/44] feat/Adapt to recent changes in ImmutableData close function (#191) --- email_app/app/components/mail_inbox.js | 2 +- email_app/app/safenet_comm.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/email_app/app/components/mail_inbox.js b/email_app/app/components/mail_inbox.js index f9e1f59..b5b9f2e 100644 --- a/email_app/app/components/mail_inbox.js +++ b/email_app/app/components/mail_inbox.js @@ -34,7 +34,7 @@ export default class MailInbox extends Component {
    - Inbox Space Used: {this.props.inboxSize}KB of {CONSTANTS.TOTAL_INBOX_SIZE}KB + Inbox Space Used: {this.props.inboxSize} of {CONSTANTS.TOTAL_INBOX_SIZE}
    @@ -174,6 +178,7 @@ export default class Auth extends Component { this.setState({ showRemapModal: false }); + this.props.clearNotification(); } showRemapModal(service, publicId) { @@ -200,7 +205,7 @@ export default class Auth extends Component { defaultValue={this.state.newPublicId} placeholder={I18n.t('messages.publicIdPlaceholder')} onPressEnter={this.createPublicId.bind(this)} /> -
    {this.props.creatingPublicId ? '' : this.props.publicIdError}
    +
    {this.props.creatingPublicId ? '' : this.props.publicIdError || this.state.publicNameErr}
    +
    + +
    { this.publicIdModal() } { this.remapModal() } - +
    ); } @@ -318,6 +335,7 @@ export default class Auth extends Component { Auth.propTypes = { connect: PropTypes.func.isRequired, + reconnect: PropTypes.func.isRequired, getAccessInfo: PropTypes.func.isRequired, getPublicNames: PropTypes.func.isRequired, getPublicContainers: PropTypes.func.isRequired, @@ -326,6 +344,7 @@ Auth.propTypes = { isConnecting: PropTypes.bool.isRequired, isConnected: PropTypes.bool.isRequired, + networkState: PropTypes.string.isRequired, connectionError: PropTypes.string, fetchingAccessInfo: PropTypes.bool.isRequired, diff --git a/web_hosting_manager/app/components/NetworkStatus.js b/web_hosting_manager/app/components/NetworkStatus.js index bfad7df..7ead0e4 100644 --- a/web_hosting_manager/app/components/NetworkStatus.js +++ b/web_hosting_manager/app/components/NetworkStatus.js @@ -2,21 +2,34 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { I18n } from 'react-redux-i18n'; +import CONSTANTS from '../constants'; export default class NetworkStatus extends Component { render() { + const { status, reconnect } = this.props; return (
    - + {' '} + {this.props.message} +
    + ) : ( +
    + + Reconnect +
    )} - >{' '} - {this.props.message}
    ); } diff --git a/web_hosting_manager/app/lib/constants.js b/web_hosting_manager/app/constants.js similarity index 77% rename from web_hosting_manager/app/lib/constants.js rename to web_hosting_manager/app/constants.js index d282087..4da391b 100644 --- a/web_hosting_manager/app/lib/constants.js +++ b/web_hosting_manager/app/constants.js @@ -1,4 +1,4 @@ -import pkg from '../package.json'; +import pkg from './package.json'; const CONSTANTS = { TAG_TYPE: { DNS: 15001, @@ -23,15 +23,13 @@ const CONSTANTS = { 'Read', 'Insert', 'Update', - 'Delete', - 'ManagePermissions' + 'Delete' ], _publicNames: [ 'Read', 'Insert', 'Update', - 'Delete', - 'ManagePermissions' + 'Delete' ] } }, @@ -48,6 +46,12 @@ const CONSTANTS = { NO_SUCH_ENTRY: -106, ENTRY_EXISTS: -107 }, - MAX_FILE_SIZE: 20 * 1024 * 1024 + MAX_FILE_SIZE: 20 * 1024 * 1024, + NETWORK_STATE: { + INIT: 'Init', + CONNECTED: 'Connected', + UNKNOWN: 'Unknown', + DISCONNECTED: 'Disconnected' + } }; export default CONSTANTS; diff --git a/web_hosting_manager/app/containers/HomePage.js b/web_hosting_manager/app/containers/HomePage.js index c4b4ed9..1a5f7eb 100644 --- a/web_hosting_manager/app/containers/HomePage.js +++ b/web_hosting_manager/app/containers/HomePage.js @@ -10,6 +10,7 @@ const mapStateToProps = (state) => { return { isConnecting: state.connection.isConnecting, isConnected: state.connection.isConnected, + networkState: state.connection.networkState, connectionError: state.connection.error, fetchingAccessInfo: state.accessInfo.fetchingAccessInfo, fetchedAccessInfo: state.accessInfo.fetchedAccessInfo, diff --git a/web_hosting_manager/app/img/nw-reload.svg b/web_hosting_manager/app/img/nw-reload.svg new file mode 100644 index 0000000..d909d53 --- /dev/null +++ b/web_hosting_manager/app/img/nw-reload.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web_hosting_manager/app/index.js b/web_hosting_manager/app/index.js index eca8abc..5b22001 100644 --- a/web_hosting_manager/app/index.js +++ b/web_hosting_manager/app/index.js @@ -8,6 +8,7 @@ import { Router, hashHistory } from 'react-router'; import { syncHistoryWithStore } from 'react-router-redux'; import { loadTranslations, setLocale, syncTranslationWithStore } from 'react-redux-i18n'; + import { initTempFolder } from './lib/temp'; import routes from './routes'; import configureStore from './store/configureStore'; @@ -40,6 +41,8 @@ const listenForAuthReponse = (event, response) => { } }; + + ipc.on('auth-response', listenForAuthReponse); ipc.on('clear-access-data', (event, res) => { diff --git a/web_hosting_manager/app/lib/Downloader.js b/web_hosting_manager/app/lib/Downloader.js index e822cfe..cb6d84b 100644 --- a/web_hosting_manager/app/lib/Downloader.js +++ b/web_hosting_manager/app/lib/Downloader.js @@ -3,7 +3,7 @@ import path from 'path'; import { getPath } from './temp'; import { shell } from 'electron'; import safeApi from './api'; -import CONSTANTS from './constants'; +import CONSTANTS from '../constants'; export default class Downloader { constructor(networkPath, callback) { diff --git a/web_hosting_manager/app/lib/api.js b/web_hosting_manager/app/lib/api.js index 6d4092d..a2ef9c1 100644 --- a/web_hosting_manager/app/lib/api.js +++ b/web_hosting_manager/app/lib/api.js @@ -8,7 +8,7 @@ import { I18n } from 'react-redux-i18n'; import Uploader from './Uploader'; import Downloader from './Downloader'; import * as utils from './utils'; -import CONSTANTS from './constants'; +import CONSTANTS from '../constants'; class SafeApi { constructor() { @@ -38,19 +38,20 @@ class SafeApi { * @param uri * @return {*} */ - connect(uri) { + connect(uri, nwStateChangeCb) { const authInfo = uri || JSON.parse(utils.localAuthInfo.get()); if (!authInfo) { // FIXME shankar - handle from action // return Promise.reject(new Error('Missing Authorisation information.')); return this.authorise(); } - return safeApp.fromAuthURI(this.APP_INFO.data, authInfo) + return safeApp.fromAuthURI(this.APP_INFO.data, authInfo, nwStateChangeCb) .then((app) => { // store Auth response if (uri) { utils.localAuthInfo.save(uri); } + nwStateChangeCb(CONSTANTS.NETWORK_STATE.CONNECTED); this.app = app; }) .catch((err) => { @@ -66,6 +67,10 @@ class SafeApi { }); } + reconnect() { + return this.app.reconnect(); + } + /** * Check access containers accessible * @return {*} diff --git a/web_hosting_manager/app/lib/tasks.js b/web_hosting_manager/app/lib/tasks.js index 54dc69b..b90371a 100644 --- a/web_hosting_manager/app/lib/tasks.js +++ b/web_hosting_manager/app/lib/tasks.js @@ -3,7 +3,7 @@ import fs from 'fs'; import { I18n } from 'react-redux-i18n'; import safeApi from './api'; -import CONSTANTS from './constants'; +import CONSTANTS from '../constants'; const parseContainerPath = (targetPath) => { if (!targetPath) { diff --git a/web_hosting_manager/app/lib/utils.js b/web_hosting_manager/app/lib/utils.js index e729e68..e5d61f3 100644 --- a/web_hosting_manager/app/lib/utils.js +++ b/web_hosting_manager/app/lib/utils.js @@ -5,7 +5,7 @@ import path from 'path'; import { I18n } from 'react-redux-i18n'; import * as Task from './tasks'; -import CONSTANTS from './constants'; +import CONSTANTS from '../constants'; class LocalAuthInfo { constructor() { diff --git a/web_hosting_manager/app/locales/en.js b/web_hosting_manager/app/locales/en.js index aaef78d..5daa85d 100644 --- a/web_hosting_manager/app/locales/en.js +++ b/web_hosting_manager/app/locales/en.js @@ -23,8 +23,10 @@ const en = { revoked: 'Application Revoked' }, networkStatus: { - connecting: 'connecting', - connected: 'connected' + connecting: 'Connecting', + disconnected: 'Disconnected', + connected: 'Connected', + unknown: 'Unknown' }, createPublicId: 'Create Public Id', noPublicIdText: 'No Public Id found. Create one to begin.', @@ -49,7 +51,8 @@ const en = { uploadingMessage: 'Uploading files', downloadingMessage: 'Downloading', loading: 'Loading...', - empty: 'Empty' + empty: 'Empty', + uploadSomeFiles: 'No files found. Please upload web files' }, messages: { emptyServiceName: 'Service name must be filled', diff --git a/web_hosting_manager/app/reducers/connection.js b/web_hosting_manager/app/reducers/connection.js index 9d54430..b64c917 100644 --- a/web_hosting_manager/app/reducers/connection.js +++ b/web_hosting_manager/app/reducers/connection.js @@ -2,11 +2,14 @@ import ACTION_TYPES from '../actions/actionTypes'; import { I18n } from 'react-redux-i18n'; +import CONSTANTS from '../constants'; const initialState = { - isConnected: false, - isConnecting: false, - error: null + isConnected: false, + isConnecting: false, + reconnecting: false, + networkState: null, + error: null }; const connection = (state: Object = initialState, action: Object) => { @@ -41,6 +44,37 @@ const connection = (state: Object = initialState, action: Object) => { error: I18n.t('messages.safeNetworkDisconnected') }; break; + case `${ACTION_TYPES.RECONNECT}_PENDING`: + state = { + ...state, + reconnecting: true, + networkState: CONSTANTS.NETWORK_STATE.INIT + }; + break; + case `${ACTION_TYPES.RECONNECT}_FULFILLED`: + state = { + ...state, + reconnecting: false, + isConnected: true, + }; + break; + + case `${ACTION_TYPES.RECONNECT}_REJECTED`: + state = { + ...state, + reconnecting: false, + isConnected: false, + error: I18n.t('messages.safeNetworkDisconnected') + }; + break; + + case ACTION_TYPES.NET_STATUS_CHANGED: + state = { + ...state, + networkState: action.state, + reconnecting: false, + }; + break; } return state; }; diff --git a/web_hosting_manager/app/styles/app.css b/web_hosting_manager/app/styles/app.css index ee40164..6301466 100644 --- a/web_hosting_manager/app/styles/app.css +++ b/web_hosting_manager/app/styles/app.css @@ -492,6 +492,35 @@ nav { display: inline-block; } +.nw-status .nw-status-b { + display: block; + width: 100%; + height: 100%; +} + +.nw-status .reconnect { + width: 24px; + height: 24px; +} + +.nw-status .reconnect .reconnect-btn { + width: 100%; + height: 100%; + background-color: #FFF; + background-image: url("../img/nw-reload.svg"); + background-position: center; + background-size: 16px; + background-repeat: no-repeat; + border: 0; + border-radius: 50%; + outline: none; + cursor: pointer; +} + +.nw-status .reconnect .reconnect-btn:hover { + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); +} + @keyframes spin { 0% { transform: rotate(0deg); From 5b5f2f3fa9b29a182e25b4d47b3bc07369fac25d Mon Sep 17 00:00:00 2001 From: Gabriel Viganotti Date: Fri, 30 Jun 2017 01:06:01 +0900 Subject: [PATCH 36/44] fix/If reconnect fails enable the re-enable the reconnet button (#197) --- email_app/app/components/home.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/email_app/app/components/home.js b/email_app/app/components/home.js index 9e10f1f..d73c43b 100755 --- a/email_app/app/components/home.js +++ b/email_app/app/components/home.js @@ -30,7 +30,8 @@ export default class Home extends Component { reconnect() { this.setState({reconnecting: true}); return this.props.reconnectApplication() - .then(() => this.setState({reconnecting: false})); + .catch((err) => 'not reconnected') + .then(() => this.setState({reconnecting: false})) } render() { From 5562f899480371755e506e02b2185d33dd6d1ed7 Mon Sep 17 00:00:00 2001 From: Shankar Date: Mon, 3 Jul 2017 14:40:32 +0530 Subject: [PATCH 37/44] fix/ui_fixes: resolve minor ui fixes (#198) Disables file explorer upload dropdown user selection. Rephrase error message on alerts --- web_hosting_manager/app/components/Auth.js | 2 +- web_hosting_manager/app/components/NetworkStatus.js | 2 +- web_hosting_manager/app/constants.js | 2 ++ web_hosting_manager/app/lib/utils.js | 12 +++++++----- web_hosting_manager/app/package.json | 6 +++--- web_hosting_manager/app/reducers/access_info.js | 6 +++++- web_hosting_manager/app/styles/app.css | 10 ++++++---- web_hosting_manager/package.json | 8 ++++---- 8 files changed, 29 insertions(+), 19 deletions(-) diff --git a/web_hosting_manager/app/components/Auth.js b/web_hosting_manager/app/components/Auth.js index ad15a82..2852513 100644 --- a/web_hosting_manager/app/components/Auth.js +++ b/web_hosting_manager/app/components/Auth.js @@ -69,7 +69,7 @@ export default class Auth extends Component { if (this.props.accessInfoError || this.props.publicNameError || this.props.publicContainersError || this.props.serviceError) { return { - title: I18n.t('label.initialising.appErrorTitle'), + title: I18n.t('label.initialising.authErrorTitle'), content: (
    { this.props.accessInfoError || this.props.publicNameError || this.props.serviceError } diff --git a/web_hosting_manager/app/components/NetworkStatus.js b/web_hosting_manager/app/components/NetworkStatus.js index 7ead0e4..25f1e68 100644 --- a/web_hosting_manager/app/components/NetworkStatus.js +++ b/web_hosting_manager/app/components/NetworkStatus.js @@ -36,6 +36,6 @@ export default class NetworkStatus extends Component { } NetworkStatus.propTypes = { - status: PropTypes.number.isRequired, + status: PropTypes.string.isRequired, message: PropTypes.string }; diff --git a/web_hosting_manager/app/constants.js b/web_hosting_manager/app/constants.js index 4da391b..0b7d1a3 100644 --- a/web_hosting_manager/app/constants.js +++ b/web_hosting_manager/app/constants.js @@ -42,7 +42,9 @@ const CONSTANTS = { REVOKED: 'revoked' }, ERROR_CODE: { + ENCODE_DECODE_ERROR: -1, SYMMETRIC_DECIPHER_FAILURE: -3, + ACCESS_DENIED: -100, NO_SUCH_ENTRY: -106, ENTRY_EXISTS: -107 }, diff --git a/web_hosting_manager/app/lib/utils.js b/web_hosting_manager/app/lib/utils.js index e5d61f3..ac23d68 100644 --- a/web_hosting_manager/app/lib/utils.js +++ b/web_hosting_manager/app/lib/utils.js @@ -1,7 +1,6 @@ import { shell } from 'electron'; -import keytar from 'keytar'; +// import keytar from 'keytar'; import fs from 'fs'; -import path from 'path'; import { I18n } from 'react-redux-i18n'; import * as Task from './tasks'; @@ -13,13 +12,16 @@ class LocalAuthInfo { this.ACCOUNT = CONSTANTS.KEY_TAR.ACCOUNT; } save(info) { - return keytar.addPassword(this.SERVICE, this.ACCOUNT, JSON.stringify(info)); + // return keytar.addPassword(this.SERVICE, this.ACCOUNT, JSON.stringify(info)); + return; } get() { - return keytar.getPassword(this.SERVICE, this.ACCOUNT); + // return keytar.getPassword(this.SERVICE, this.ACCOUNT); + return; } clear() { - return keytar.deletePassword(this.SERVICE, this.ACCOUNT); + // return keytar.deletePassword(this.SERVICE, this.ACCOUNT); + return; } } diff --git a/web_hosting_manager/app/package.json b/web_hosting_manager/app/package.json index b40416f..647de97 100644 --- a/web_hosting_manager/app/package.json +++ b/web_hosting_manager/app/package.json @@ -1,9 +1,9 @@ { - "name": "safe_hosting_manager", - "productName": "SAFE Hosting Manager", + "name": "web_hosting_manager", + "productName": "Web Hosting Manager", "version": "0.1.0", "description": "Application to manage web hosting in SAFE Network", - "identifier": "net.maidsafe.safehostingmanager", + "identifier": "net.maidsafe.webhostingmanager", "main": "./main.js", "author": { "name": "MaidSafe", diff --git a/web_hosting_manager/app/reducers/access_info.js b/web_hosting_manager/app/reducers/access_info.js index 569121f..a16c2b3 100644 --- a/web_hosting_manager/app/reducers/access_info.js +++ b/web_hosting_manager/app/reducers/access_info.js @@ -3,6 +3,7 @@ import ACTION_TYPES from '../actions/actionTypes'; import { I18n } from 'react-redux-i18n'; import { trimErrorMsg } from '../utils/app_utils'; +import CONSTANTS from '../constants'; const initialState = { fetchingAccessInfo: false, @@ -35,10 +36,13 @@ const accessInfo = (state: Object = initialState, action: Object) => { break; case `${ACTION_TYPES.FETCH_ACCESS_INFO}_REJECTED`: + console.log('fetch Error', action.payload); + let errorMsg = (action.payload.code === CONSTANTS.ERROR_CODE.ENCODE_DECODE_ERROR) ? + I18n.t('label.initialising.revoked') : trimErrorMsg(action.payload.message); state = { ...state, fetchingAccessInfo: false, - error: I18n.t('messages.fetchingAccessFailed', { error: trimErrorMsg(action.payload.message) }) + error: I18n.t('messages.fetchingAccessFailed', { error: errorMsg }) }; break; } diff --git a/web_hosting_manager/app/styles/app.css b/web_hosting_manager/app/styles/app.css index 6301466..32f8004 100644 --- a/web_hosting_manager/app/styles/app.css +++ b/web_hosting_manager/app/styles/app.css @@ -1,3 +1,9 @@ +* { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} body { position: relative; height: 100vh; @@ -7,10 +13,6 @@ body { padding: 0; margin: 0; background-color: #f4f4f4; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; } #root, #root > div { diff --git a/web_hosting_manager/package.json b/web_hosting_manager/package.json index 6ae46cf..ad8cd68 100644 --- a/web_hosting_manager/package.json +++ b/web_hosting_manager/package.json @@ -1,6 +1,6 @@ { - "name": "safe_hosting_manager", - "productName": "SAFE Hosting Manager", + "name": "web_hosting_manager", + "productName": "Web Hosting Manager", "version": "0.1.0", "description": "Application to host web files in the SAFE Network", "main": "main.js", @@ -59,10 +59,10 @@ }, "protocols": [ { - "name": "safehostingmanager", + "name": "webhostingmanager", "role": "Viewer", "schemes": [ - "safe-bmv0lm1hawrzywzllnnhzmvob3n0aw5nbwfuywdlcg" + "safe-bmv0lm1hawrzywzllndlymhvc3rpbmdtyw5hz2vy" ] } ] From 3d497f251ffc07c4cccb6e4089cd6ec176dff17a Mon Sep 17 00:00:00 2001 From: Shankar Date: Mon, 3 Jul 2017 14:57:51 +0530 Subject: [PATCH 38/44] feat/keytar: remove key tar deps (#199) Removed Keytar dependency --- web_hosting_manager/README.md | 3 --- web_hosting_manager/app/package.json | 1 - 2 files changed, 4 deletions(-) diff --git a/web_hosting_manager/README.md b/web_hosting_manager/README.md index 4345736..c8bf17a 100644 --- a/web_hosting_manager/README.md +++ b/web_hosting_manager/README.md @@ -1,8 +1,5 @@ # SAFE Hosting Manager -#### Prerequisites -> SAFE Hosting Manager uses **[keytar](https://www.npmjs.com/package/keytar)** module as its dependency. Please install the prerequisites mentioned [here](https://www.npmjs.com/package/keytar#installing) based on the platform. - ## Install * **Note: requires a node version 6.5.0 and an npm version 3.10.3** diff --git a/web_hosting_manager/app/package.json b/web_hosting_manager/app/package.json index 647de97..194224f 100644 --- a/web_hosting_manager/app/package.json +++ b/web_hosting_manager/app/package.json @@ -13,7 +13,6 @@ "license": "MIT", "dependencies": { "classnames": "^2.2.5", - "keytar": "^3.0.2", "safe-app": "git+https://github.com/maidsafe/safe_app_nodejs.git" } } From 33a02dc61cead4ee9dd2887c2ba5567e3497fa97 Mon Sep 17 00:00:00 2001 From: Krishna Date: Mon, 3 Jul 2017 14:02:25 +0000 Subject: [PATCH 39/44] feat/no_local_key_trace: remove local storage (#200) Remove local storage to ensure the keys are not stored locally --- email_app/app/utils/app_utils.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/email_app/app/utils/app_utils.js b/email_app/app/utils/app_utils.js index fc424d9..e7a67eb 100644 --- a/email_app/app/utils/app_utils.js +++ b/email_app/app/utils/app_utils.js @@ -5,20 +5,22 @@ import { CONSTANTS } from '../constants'; import sodium from 'libsodium-wrappers'; export const getAuthData = () => { - let authData = window.JSON.parse( - window.localStorage.getItem(CONSTANTS.LOCAL_AUTH_DATA_KEY) - ); - return authData; + // let authData = window.JSON.parse( + // window.localStorage.getItem(CONSTANTS.LOCAL_AUTH_DATA_KEY) + // ); + // return authData; + return; }; export const saveAuthData = (authData) => { - return window.localStorage.setItem(CONSTANTS.LOCAL_AUTH_DATA_KEY, - window.JSON.stringify(authData) - ); + return; + // window.localStorage.setItem(CONSTANTS.LOCAL_AUTH_DATA_KEY, + // window.JSON.stringify(authData) + // ); }; export const clearAuthData = () => { - window.localStorage.removeItem(CONSTANTS.LOCAL_AUTH_DATA_KEY); + // window.localStorage.removeItem(CONSTANTS.LOCAL_AUTH_DATA_KEY); }; export const genServiceInfo = (emailId) => { From 55c36c3273ca9febf82b149ef0d764d16b85ba39 Mon Sep 17 00:00:00 2001 From: Gabriel Viganotti Date: Fri, 7 Jul 2017 23:55:52 +0900 Subject: [PATCH 40/44] feat/Display a loading spinner when processing some task (#202) * feat/Display a loading spinner when the app is processing some task * refactor/Refactoring some code which handles the redux actions of different pages * feat/Enhancements to the spinner and modal dialog --- email_app/app/actions/actionTypes.js | 1 - .../app/actions/create_account_actions.js | 4 +- email_app/app/actions/mail_actions.js | 13 +- email_app/app/components/create_account.js | 3 +- email_app/app/components/home.js | 70 ++++---- email_app/app/components/initializer.js | 1 + email_app/app/components/mail_list.js | 160 ++++++++---------- .../app/containers/compose_mail_container.js | 6 +- .../containers/create_account_container.js | 2 +- email_app/app/containers/home_container.js | 7 +- .../app/containers/mail_inbox_container.js | 4 +- email_app/app/less/authenticate.less | 2 + email_app/app/less/base.less | 2 + email_app/app/less/buttons.less | 4 +- email_app/app/less/compose_mail.less | 2 + email_app/app/less/form.less | 2 + email_app/app/less/home.less | 2 + email_app/app/less/list.less | 2 + email_app/app/less/variables.less | 16 ++ email_app/app/less/view_mail.less | 2 + email_app/app/reducers/create_account.js | 14 +- email_app/app/reducers/initialiser.js | 45 ++++- email_app/app/reducers/mail.js | 7 - email_app/package.json | 3 +- 24 files changed, 205 insertions(+), 169 deletions(-) diff --git a/email_app/app/actions/actionTypes.js b/email_app/app/actions/actionTypes.js index 2915ad6..a156af1 100644 --- a/email_app/app/actions/actionTypes.js +++ b/email_app/app/actions/actionTypes.js @@ -15,7 +15,6 @@ const ACTION_TYPES = { // Mail Inbox PUSH_MAIL: 'PUSH_MAIL', MAIL_PROCESSING: 'MAIL_PROCESSING', - CLEAR_MAIL_PROCESSING: 'CLEAR_MAIL_PROCESSING', SET_ACTIVE_MAIL: 'SET_ACTIVE_MAIL', CANCEL_COMPOSE: 'CANCEL_COMPOSE', diff --git a/email_app/app/actions/create_account_actions.js b/email_app/app/actions/create_account_actions.js index cb920ea..37f0c35 100644 --- a/email_app/app/actions/create_account_actions.js +++ b/email_app/app/actions/create_account_actions.js @@ -12,6 +12,6 @@ export const createAccount = (emailId) => { }; export const createAccountError = (error) => ({ - type: ACTION_TYPES.CREATE_ACCOUNT_ERROR, - error + type: ACTION_TYPES.CREATE_ACCOUNT, + payload: Promise.reject(error) }); diff --git a/email_app/app/actions/mail_actions.js b/email_app/app/actions/mail_actions.js index 13fec7f..ae164b0 100644 --- a/email_app/app/actions/mail_actions.js +++ b/email_app/app/actions/mail_actions.js @@ -6,9 +6,8 @@ export const sendEmail = (email, to) => { let app = getState().initializer.app; return dispatch({ type: ACTION_TYPES.MAIL_PROCESSING, + msg: 'Sending email...', payload: storeEmail(app, email, to) - .then(() => dispatch(clearMailProcessing)) - .then(() => Promise.resolve()) }); }; }; @@ -18,9 +17,8 @@ export const saveEmail = (account, key) => { let app = getState().initializer.app; return dispatch({ type: ACTION_TYPES.MAIL_PROCESSING, + msg: 'Saving email...', payload: archiveEmail(app, account, key) - .then(() => dispatch(clearMailProcessing)) - .then(() => Promise.resolve()) }); }; }; @@ -30,17 +28,12 @@ export const deleteEmail = (container, key) => { let app = getState().initializer.app; return dispatch({ type: ACTION_TYPES.MAIL_PROCESSING, + msg: 'Deleting email...', payload: removeEmail(app, container, key) - .then(() => dispatch(clearMailProcessing)) - .then(() => Promise.resolve()) }); }; }; -export const clearMailProcessing = _ => ({ - type: ACTION_TYPES.CLEAR_MAIL_PROCESSING -}); - export const cancelCompose = _ => ({ type: ACTION_TYPES.CANCEL_COMPOSE }); diff --git a/email_app/app/components/create_account.js b/email_app/app/components/create_account.js index ff6d113..b64e571 100644 --- a/email_app/app/components/create_account.js +++ b/email_app/app/components/create_account.js @@ -41,7 +41,6 @@ export default class CreateAccount extends Component { render() { const { processing, error } = this.props; - return (
    @@ -54,7 +53,7 @@ export default class CreateAccount extends Component {
    Email Id must be less than {CONSTANTS.EMAIL_ID_MAX_LENGTH} characters. (This is just a restriction in this tutorial)
    - +

    {error.message}

    diff --git a/email_app/app/components/home.js b/email_app/app/components/home.js index d73c43b..11bf3d5 100755 --- a/email_app/app/components/home.js +++ b/email_app/app/components/home.js @@ -1,59 +1,63 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { Link, IndexLink } from 'react-router'; -import Modal from 'react-modal'; +import {ModalDialog, ModalPortal, ModalBackground} from 'react-modal-dialog'; +import ReactSpinner from 'react-spinjs'; import className from 'classnames'; import pkg from '../../package.json'; import { CONSTANTS } from '../constants'; -const modalStyles = { - content : { - top : '50%', - left : '50%', - right : 'auto', - bottom : 'auto', - marginRight : '-50%', - transform : 'translate(-50%, -50%)' - } -}; - export default class Home extends Component { constructor() { super(); - this.state = { - reconnecting: false - }; this.reconnect = this.reconnect.bind(this); } reconnect() { - this.setState({reconnecting: true}); - return this.props.reconnectApplication() - .catch((err) => 'not reconnected') - .then(() => this.setState({reconnecting: false})) + const { reconnectApplication, accounts, refreshEmail } = this.props; + return reconnectApplication() + .then(() => refreshEmail(accounts), + (err) => 'failed reconnecting'); } render() { const { router } = this.context; - const { coreData, inboxSize, savedSize, network_status } = this.props; + const { coreData, inboxSize, savedSize, network_status, processing } = this.props; - const isNetworkDisconnected = (network_status !== CONSTANTS.NET_STATUS_CONNECTED); + const isModalOpen = processing.state || (network_status !== CONSTANTS.NET_STATUS_CONNECTED); + const spinnerBackgroundStyle = { + zIndex: '5', + position: 'fixed', + height: '100%', + width: '100%', + opacity: '0.75', + backgroundColor: 'white' + } return (
    - -
    -
    The application hast lost network connection.

    -
    Make sure the network link is up before trying to reconnect.

    - -
    -
    + { + isModalOpen && + + { + processing.state ? +
    + +
    + : + + +
    +
    The application hast lost network connection.

    +
    Make sure the network link is up before trying to reconnect.

    + +
    +
    +
    + } +
    + }
    diff --git a/email_app/app/components/initializer.js b/email_app/app/components/initializer.js index 53c98be..a1f2e3c 100644 --- a/email_app/app/components/initializer.js +++ b/email_app/app/components/initializer.js @@ -28,6 +28,7 @@ export default class Initializer extends Component { refreshConfig() { const { setInitializerTask, refreshConfig } = this.props; setInitializerTask(MESSAGES.INITIALIZE.CHECK_CONFIGURATION); + return refreshConfig() .then((_) => { if (Object.keys(this.props.accounts).length > 0) { diff --git a/email_app/app/components/mail_list.js b/email_app/app/components/mail_list.js index c680375..8a92c55 100644 --- a/email_app/app/components/mail_list.js +++ b/email_app/app/components/mail_list.js @@ -9,27 +9,12 @@ export default class MailList extends Component { super(); this.listColors = {}; this.activeType = null; - this.goBack = this.goBack.bind(this); this.refreshEmail = this.refreshEmail.bind(this); this.handleDeleteFromInbox = this.handleDeleteFromInbox.bind(this); this.handleDeleteSaved = this.handleDeleteSaved.bind(this); this.handleSave = this.handleSave.bind(this); } - goBack() { - const { router } = this.context; - - switch (this.activeType) { - case CONSTANTS.HOME_TABS.INBOX: { - return this.props.refreshEmail(); - } - case CONSTANTS.HOME_TABS.SAVED: { - router.push('/home'); - return router.push('/saved'); - } - } - } - refreshEmail(account) { this.props.refreshEmail(account) .catch((error) => { @@ -76,87 +61,82 @@ export default class MailList extends Component { render() { const self = this; - const { processing, coreData, error, inboxSize, inbox, savedSize, saved } = this.props; + const { coreData, error, inboxSize, inbox, savedSize, saved } = this.props; let container = null; - if (processing) { - container =
  • Loading...
  • - } else if (Object.keys(error).length > 0) { - container =
  • Error in fetching emails!
  • - } else { - if (inbox) { - this.activeType = CONSTANTS.HOME_TABS.INBOX; - container = ( -
    - { - inboxSize === 0 ?
  • Inbox empty
  • : Object.keys(coreData.inbox).map((key) => { - let mail = coreData.inbox[key]; - if (!self.listColors.hasOwnProperty(mail.from)) { - self.listColors[mail.from] = `bg-color-${Object.keys(self.listColors).length % 10}` - } - return ( -
  • -
    - {mail.from[0]} -
    -
    -

    {mail.from}

    -

    {dateformat(new Date(mail.time), CONSTANTS.DATE_FORMAT)}

    -

    {mail.subject}

    -

    {mail.body}

    + if (inbox) { + this.activeType = CONSTANTS.HOME_TABS.INBOX; + container = ( +
    + { + inboxSize === 0 ?
  • Inbox empty
  • : Object.keys(coreData.inbox).map((key) => { + let mail = coreData.inbox[key]; + if (!self.listColors.hasOwnProperty(mail.from)) { + self.listColors[mail.from] = `bg-color-${Object.keys(self.listColors).length % 10}` + } + return ( +
  • +
    + {mail.from[0]} +
    +
    +

    {mail.from}

    +

    {dateformat(new Date(mail.time), CONSTANTS.DATE_FORMAT)}

    +

    {mail.subject}

    +

    {mail.body}

    +
    +
    +
    +
    -
    -
    - -
    -
    - -
    +
    +
    -
  • - ) - }) - } -
    - ); - } - if (saved) { - this.activeType = CONSTANTS.HOME_TABS.SAVED; - container = ( -
    - { - savedSize === 0 ?
  • Saved empty
  • : Object.keys(coreData.saved).map((key) => { - let mail = coreData.saved[key]; - if (!mail) { - return; - } - if (!self.listColors.hasOwnProperty(mail.from)) { - self.listColors[mail.from] = `bg-color-${Object.keys(self.listColors).length % 10}` - } - return ( -
  • -
    - {mail.from[0]} -
    -
    -

    {mail.from}

    -

    {dateformat(new Date(mail.time), CONSTANTS.DATE_FORMAT)}

    -

    {mail.subject}

    -

    {mail.body}

    -
    -
    -
    - -
    +
    +
  • + ) + }) + } +
    + ); + } + if (saved) { + this.activeType = CONSTANTS.HOME_TABS.SAVED; + container = ( +
    + { + savedSize === 0 ?
  • Saved empty
  • : Object.keys(coreData.saved).map((key) => { + let mail = coreData.saved[key]; + if (!mail) { + return; + } + if (!self.listColors.hasOwnProperty(mail.from)) { + self.listColors[mail.from] = `bg-color-${Object.keys(self.listColors).length % 10}` + } + return ( +
  • +
    + {mail.from[0]} +
    +
    +

    {mail.from}

    +

    {dateformat(new Date(mail.time), CONSTANTS.DATE_FORMAT)}

    +

    {mail.subject}

    +

    {mail.body}

    +
    +
    +
    +
    -
  • - ) - }) - } -
    - ); - } +
    + + ) + }) + } +
    + ); } + return (
      {container} diff --git a/email_app/app/containers/compose_mail_container.js b/email_app/app/containers/compose_mail_container.js index 11ae136..4430b14 100644 --- a/email_app/app/containers/compose_mail_container.js +++ b/email_app/app/containers/compose_mail_container.js @@ -1,20 +1,18 @@ import { connect } from 'react-redux'; import ComposeMail from '../components/compose_mail'; -import { cancelCompose, sendEmail, clearMailProcessing } from '../actions/mail_actions'; +import { cancelCompose, sendEmail } from '../actions/mail_actions'; const mapStateToProps = state => { return { app: state.initializer.app, fromMail: state.initializer.coreData.id, - error: state.mail.error, - processing: state.mail.processing + error: state.mail.error }; }; const mapDispatchToProps = dispatch => { return { sendEmail: (email, to) => (dispatch(sendEmail(email, to))), - clearMailProcessing: _ => (dispatch(clearMailProcessing())), cancelCompose: _ => dispatch(cancelCompose()) }; }; diff --git a/email_app/app/containers/create_account_container.js b/email_app/app/containers/create_account_container.js index 3012740..bb74fd6 100644 --- a/email_app/app/containers/create_account_container.js +++ b/email_app/app/containers/create_account_container.js @@ -6,7 +6,7 @@ import { storeNewAccount } from '../actions/initializer_actions'; const mapStateToProps = state => { return { error: state.createAccount.error, - processing: state.createAccount.processing, + processing: state.initializer.processing, newAccount: state.createAccount.newAccount, coreData: state.initializer.coreData }; diff --git a/email_app/app/containers/home_container.js b/email_app/app/containers/home_container.js index 6c38f0c..4644919 100644 --- a/email_app/app/containers/home_container.js +++ b/email_app/app/containers/home_container.js @@ -1,19 +1,22 @@ import { connect } from 'react-redux'; import Home from '../components/home'; -import { reconnectApplication } from '../actions/initializer_actions'; +import { reconnectApplication, refreshEmail } from '../actions/initializer_actions'; const mapStateToProps = state => { return { coreData: state.initializer.coreData, + accounts: state.initializer.accounts, inboxSize: state.initializer.inboxSize, savedSize: state.initializer.savedSize, - network_status: state.initializer.network_status + network_status: state.initializer.network_status, + processing: state.initializer.processing }; }; const mapDispatchToProps = dispatch => { return { reconnectApplication: () => (dispatch(reconnectApplication())), + refreshEmail: (account) => (dispatch(refreshEmail(account))), }; }; diff --git a/email_app/app/containers/mail_inbox_container.js b/email_app/app/containers/mail_inbox_container.js index 752ae40..9f19423 100644 --- a/email_app/app/containers/mail_inbox_container.js +++ b/email_app/app/containers/mail_inbox_container.js @@ -1,11 +1,10 @@ import { connect } from 'react-redux'; import MailInbox from '../components/mail_inbox'; -import { refreshInbox, clearMailProcessing, deleteEmail, saveEmail } from '../actions/mail_actions'; +import { refreshInbox, deleteEmail, saveEmail } from '../actions/mail_actions'; import { refreshEmail } from '../actions/initializer_actions'; const mapStateToProps = state => { return { - processing: state.mail.processing, error: state.mail.error, coreData: state.initializer.coreData, inboxSize: state.initializer.inboxSize, @@ -19,7 +18,6 @@ const mapDispatchToProps = dispatch => { return { refreshEmail: (account) => (dispatch(refreshEmail(account))), deleteEmail: (account, key) => (dispatch(deleteEmail(account, key))), - clearMailProcessing: () => (dispatch(clearMailProcessing())), saveEmail: (account, key) => (dispatch(saveEmail(account, key))) }; }; diff --git a/email_app/app/less/authenticate.less b/email_app/app/less/authenticate.less index 585bfc8..bfa7ef1 100644 --- a/email_app/app/less/authenticate.less +++ b/email_app/app/less/authenticate.less @@ -1,3 +1,5 @@ +@import "variables.less"; + .create-account { width: 100%; height: 100%; diff --git a/email_app/app/less/base.less b/email_app/app/less/base.less index cffcd19..02eb74f 100644 --- a/email_app/app/less/base.less +++ b/email_app/app/less/base.less @@ -1,3 +1,5 @@ +@import "variables.less"; + .noselect { -webkit-touch-callout: none; /* iOS Safari */ -webkit-user-select: none; /* Chrome/Safari/Opera */ diff --git a/email_app/app/less/buttons.less b/email_app/app/less/buttons.less index cbdff7a..c4328a9 100644 --- a/email_app/app/less/buttons.less +++ b/email_app/app/less/buttons.less @@ -1,3 +1,5 @@ +@import "variables.less"; + .btn { margin: 0; padding: 8px 16px; @@ -27,5 +29,3 @@ opacity: 0.5; } } - - diff --git a/email_app/app/less/compose_mail.less b/email_app/app/less/compose_mail.less index 671f9bd..6523108 100644 --- a/email_app/app/less/compose_mail.less +++ b/email_app/app/less/compose_mail.less @@ -1,3 +1,5 @@ +@import "variables.less"; + .compose-mail { padding: 0; .compose-mail-b { diff --git a/email_app/app/less/form.less b/email_app/app/less/form.less index f8bf811..391c57e 100644 --- a/email_app/app/less/form.less +++ b/email_app/app/less/form.less @@ -1,3 +1,5 @@ +@import "variables.less"; + .form { .inp-grp { margin: 40px 0; diff --git a/email_app/app/less/home.less b/email_app/app/less/home.less index 10be0ab..23f7dd2 100755 --- a/email_app/app/less/home.less +++ b/email_app/app/less/home.less @@ -1,3 +1,5 @@ +@import "variables.less"; + .home { background-color: @gray-200; position: relative; diff --git a/email_app/app/less/list.less b/email_app/app/less/list.less index 2a59ed4..58122e4 100644 --- a/email_app/app/less/list.less +++ b/email_app/app/less/list.less @@ -1,3 +1,5 @@ +@import "variables.less"; + .mail-list { padding-bottom: 126px; .mail-list-head { diff --git a/email_app/app/less/variables.less b/email_app/app/less/variables.less index 2a12152..581e61d 100644 --- a/email_app/app/less/variables.less +++ b/email_app/app/less/variables.less @@ -7,3 +7,19 @@ @gray-200: #EEEEEE; @red-100: #e26464; + +// The clearfix mixin +.clearfix() { + &:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; + } +} + +// Apply clearfix on .parent +.parent { + .clearfix(); +} diff --git a/email_app/app/less/view_mail.less b/email_app/app/less/view_mail.less index ba8c11b..25cbb73 100644 --- a/email_app/app/less/view_mail.less +++ b/email_app/app/less/view_mail.less @@ -1,3 +1,5 @@ +@import "variables.less"; + .view-mail { background-color: @gray-200; position: absolute; diff --git a/email_app/app/reducers/create_account.js b/email_app/app/reducers/create_account.js index 80a2df6..deaae75 100644 --- a/email_app/app/reducers/create_account.js +++ b/email_app/app/reducers/create_account.js @@ -1,22 +1,20 @@ import ACTION_TYPES from '../actions/actionTypes'; const initialState = { - processing: false, error: {}, newAccount: null }; const createAccount = (state = initialState, action) => { switch (action.type) { - case `${ACTION_TYPES.CREATE_ACCOUNT}_LOADING`: - return { ...state, processing: true }; + case `${ACTION_TYPES.CREATE_ACCOUNT}_ERROR`: + return { ...state, error: action.payload }; break; - case `${ACTION_TYPES.CREATE_ACCOUNT}_SUCCESS`: { - return { ...state, processing: false, newAccount: action.payload }; + case `${ACTION_TYPES.CREATE_ACCOUNT}_SUCCESS`: + return { ...state, newAccount: action.payload }; break; - } - case ACTION_TYPES.CREATE_ACCOUNT_ERROR: - return { ...state, error: action.error, processing: false }; + case `${ACTION_TYPES.STORE_NEW_ACCOUNT}_ERROR`: + return { ...state, error: action.payload }; break; default: return state; diff --git a/email_app/app/reducers/initialiser.js b/email_app/app/reducers/initialiser.js index 243a43a..6c3b6b4 100644 --- a/email_app/app/reducers/initialiser.js +++ b/email_app/app/reducers/initialiser.js @@ -4,6 +4,10 @@ import { MESSAGES, APP_STATUS, CONSTANTS, SAFE_APP_ERROR_CODES } from '../consta const initialState = { app_status: null, network_status: null, + processing: { + state: false, + msg: null + }, app: null, tasks: [], accounts: [], @@ -44,8 +48,17 @@ const initializer = (state = initialState, action) => { case ACTION_TYPES.NET_STATUS_CHANGED: return { ...state, network_status: action.payload }; break; + case `${ACTION_TYPES.RECONNECT_APP}_LOADING`: + return { ...state, processing: { state: true, msg: 'Reconnecting...' } }; + break; + case `${ACTION_TYPES.RECONNECT_APP}_ERROR`: + return { ...state, processing: { state: false, msg: null } }; + break; case `${ACTION_TYPES.RECONNECT_APP}_SUCCESS`: - return { ...state, network_status: CONSTANTS.NET_STATUS_CONNECTED }; + return { ...state, + network_status: CONSTANTS.NET_STATUS_CONNECTED, + processing: { state: false, msg: null } + }; break; case `${ACTION_TYPES.GET_CONFIG}_LOADING`: return { ...state, app_status: APP_STATUS.READING_CONFIG }; @@ -57,19 +70,45 @@ const initializer = (state = initialState, action) => { app_status: APP_STATUS.READY }; break; + case `${ACTION_TYPES.CREATE_ACCOUNT}_LOADING`: + return { ...state, processing: { state: true, msg: 'Creating email ID...' } }; + break; + case `${ACTION_TYPES.CREATE_ACCOUNT}_ERROR`: + case `${ACTION_TYPES.CREATE_ACCOUNT}_SUCCESS`: + return { ...state, processing: { state: false, msg: null } }; + break; + case `${ACTION_TYPES.STORE_NEW_ACCOUNT}_LOADING`: + return { ...state, processing: { state: true, msg: 'Storing email info...' } }; + break; case `${ACTION_TYPES.STORE_NEW_ACCOUNT}_SUCCESS`: return { ...state, accounts: action.payload, - coreData: { ...state.coreData, id: action.payload.id } + coreData: { ...state.coreData, id: action.payload.id }, + processing: { state: false, msg: null } }; break; + case `${ACTION_TYPES.STORE_NEW_ACCOUNT}_ERROR`: + return { ...state, processing: { state: false, msg: null } }; + break; case `${ACTION_TYPES.REFRESH_EMAIL}_LOADING`: return { ...state, coreData: { ...state.coreData, inbox: [], saved: [] }, inboxSize: 0, - savedSize: 0 + savedSize: 0, + processing: { state: true, msg: 'Reading emails...' } }; break; + case `${ACTION_TYPES.REFRESH_EMAIL}_SUCCESS`: + case `${ACTION_TYPES.REFRESH_EMAIL}_ERROR`: + return { ...state, processing: { state: false, msg: null } }; + break; + case `${ACTION_TYPES.MAIL_PROCESSING}_LOADING`: + return { ...state, processing: { state: true, msg: action.msg } }; + break; + case `${ACTION_TYPES.MAIL_PROCESSING}_SUCCESS`: + case `${ACTION_TYPES.MAIL_PROCESSING}_ERROR`: + return { ...state, processing: { state: false, msg: null } }; + break; case ACTION_TYPES.PUSH_TO_INBOX: { let inbox = Object.assign({}, state.coreData.inbox, action.payload); return { ...state, diff --git a/email_app/app/reducers/mail.js b/email_app/app/reducers/mail.js index bef2630..aafa1d2 100644 --- a/email_app/app/reducers/mail.js +++ b/email_app/app/reducers/mail.js @@ -1,18 +1,11 @@ import ACTION_TYPES from '../actions/actionTypes'; const initialState = { - processing: false, error: {} }; const mail = (state = initialState, action) => { switch (action.type) { - case ACTION_TYPES.MAIL_PROCESSING: - return { ...state, processing: true }; - break; - case ACTION_TYPES.CLEAR_MAIL_PROCESSING: - return { ...state, processing: false }; - break; case ACTION_TYPES.CANCEL_COMPOSE: return { ...state, error: Object.assign({}) }; break; diff --git a/email_app/package.json b/email_app/package.json index 01d67f4..5d93271 100644 --- a/email_app/package.json +++ b/email_app/package.json @@ -114,10 +114,11 @@ "react": "^15.4.2", "react-dom": "^15.4.2", "react-hot-loader": "^3.0.0-beta.6", - "react-modal": "^2.1.0", + "react-modal-dialog": "^4.0.7", "react-redux": "^5.0.3", "react-router": "^3.0.2", "react-router-redux": "^4.0.8", + "react-spinjs": "^3.0.0", "redux": "^3.6.0", "redux-axios-middleware": "^2.0.0", "redux-promise-middleware": "^4.2.0", From f531f485dcca16871e0ce7069d8dcd520e4ff173 Mon Sep 17 00:00:00 2001 From: Gabriel Viganotti Date: Wed, 12 Jul 2017 17:53:35 +0900 Subject: [PATCH 41/44] MAID-2111 feat/Support multiple email ids (#203) * feat/Creating actions and functions to fetch all email ids from the _publicNames * feat/Adapt app configuation data to a new format to store multiple email ids * fix/Minor fix in fetch EmailIds and when adding a service to an existing publicId * docs/Update design diagram and minor refactoring, including the use of sha3 * select email UI integrated * refactor/Refactoring to have consistency in variables naming style --- email_app/app/actions/actionTypes.js | 1 + email_app/app/actions/initializer_actions.js | 18 +- email_app/app/app.html | 1 + email_app/app/components/create_account.js | 114 +++++++-- email_app/app/components/home.js | 10 +- email_app/app/components/initializer.js | 39 ++- email_app/app/components/mail_inbox.js | 7 +- email_app/app/components/mail_list.js | 21 +- email_app/app/constants.js | 16 +- .../app/containers/compose_mail_container.js | 2 +- .../containers/create_account_container.js | 10 +- email_app/app/containers/home_container.js | 4 +- .../app/containers/initializer_container.js | 14 +- .../app/containers/mail_inbox_container.js | 2 +- .../app/containers/mail_saved_container.js | 2 +- email_app/app/less/authenticate.less | 75 ++++-- email_app/app/reducers/initialiser.js | 37 ++- email_app/app/safenet_comm.js | 235 +++++++++++------- email_app/app/utils/app_utils.js | 9 +- email_app/design/EmailApp-DataModel.png | Bin 83136 -> 95061 bytes email_app/design/EmailApp-DataModel.xml | 2 +- email_app/package.json | 1 + 22 files changed, 399 insertions(+), 221 deletions(-) diff --git a/email_app/app/actions/actionTypes.js b/email_app/app/actions/actionTypes.js index a156af1..2c617c7 100644 --- a/email_app/app/actions/actionTypes.js +++ b/email_app/app/actions/actionTypes.js @@ -1,6 +1,7 @@ const ACTION_TYPES = { // Initializer AUTHORISE_APP: 'AUTHORISE_APP', + FETCH_EMAIL_IDS: 'FETCH_EMAIL_IDS', GET_CONFIG: 'GET_CONFIG', REFRESH_EMAIL: 'REFRESH_EMAIL', SET_INITIALIZER_TASK: 'SET_INITIALIZER_TASK', diff --git a/email_app/app/actions/initializer_actions.js b/email_app/app/actions/initializer_actions.js index 466a159..453a839 100644 --- a/email_app/app/actions/initializer_actions.js +++ b/email_app/app/actions/initializer_actions.js @@ -1,6 +1,6 @@ import ACTION_TYPES from './actionTypes'; -import { authApp, connect, reconnect, readConfig, writeConfig, - readInboxEmails, readArchivedEmails } from '../safenet_comm'; +import { authApp, connect, reconnect, fetchEmailIds, readConfig, + writeConfig, readInboxEmails, readArchivedEmails } from '../safenet_comm'; export const setInitializerTask = (task) => ({ type: ACTION_TYPES.SET_INITIALIZER_TASK, @@ -56,12 +56,22 @@ export const reconnectApplication = () => { }; }; -export const refreshConfig = () => { +export const getEmailIds = () => { + return function (dispatch, getState) { + let app = getState().initializer.app; + return dispatch({ + type: ACTION_TYPES.FETCH_EMAIL_IDS, + payload: fetchEmailIds(app) + }); + }; +}; + +export const refreshConfig = (emailId) => { return function (dispatch, getState) { let app = getState().initializer.app; return dispatch({ type: ACTION_TYPES.GET_CONFIG, - payload: readConfig(app) + payload: readConfig(app, emailId) }); }; }; diff --git a/email_app/app/app.html b/email_app/app/app.html index 801f634..ce19cbe 100755 --- a/email_app/app/app.html +++ b/email_app/app/app.html @@ -8,6 +8,7 @@ + diff --git a/email_app/app/components/create_account.js b/email_app/app/components/create_account.js index b64e571..74804b2 100644 --- a/email_app/app/components/create_account.js +++ b/email_app/app/components/create_account.js @@ -1,6 +1,8 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { MESSAGES, CONSTANTS, SAFE_APP_ERROR_CODES } from '../constants'; +import { ModalPortal } from 'react-modal-dialog'; +import ReactSpinner from 'react-spinjs'; export default class CreateAccount extends Component { constructor() { @@ -8,13 +10,14 @@ export default class CreateAccount extends Component { this.errMrg = null; this.handleCreateAccount = this.handleCreateAccount.bind(this); this.storeCreatedAccount = this.storeCreatedAccount.bind(this); + this.handleChooseAccount = this.handleChooseAccount.bind(this); } storeCreatedAccount() { const { newAccount, storeNewAccount, createAccountError } = this.props; return storeNewAccount(newAccount) - .then((_) => this.context.router.push('/home')) - .catch((e) => createAccountError(new Error(e))); + .then((_) => this.context.router.push('/home')) + .catch((e) => createAccountError(new Error(e))); } handleCreateAccount(e) { @@ -30,32 +33,105 @@ export default class CreateAccount extends Component { } return createAccount(emailId) - .then(this.storeCreatedAccount) - .catch((err) => { - if (err.code === SAFE_APP_ERROR_CODES.ERR_DATA_EXISTS) { - return createAccountError(new Error(MESSAGES.EMAIL_ALREADY_TAKEN)); - } - return createAccountError(err); - }); + .then(this.storeCreatedAccount) + .catch((err) => { + if (err.code === SAFE_APP_ERROR_CODES.ERR_DATA_EXISTS + || err.code === SAFE_APP_ERROR_CODES.ENTRY_ALREADY_EXISTS) { + return createAccountError(new Error(MESSAGES.EMAIL_ALREADY_TAKEN)); + } + return createAccountError(err); + }); + }; + + handleChooseAccount(e) { + e.preventDefault(); + const { refreshConfig, createAccountError } = this.props; + const emailId = this.emailSelected.value; + + return refreshConfig(emailId) + .then((_) => this.context.router.push('/home')) + .catch((e) => createAccountError(new Error(e))); }; render() { - const { processing, error } = this.props; + const { emailIds, networkStatus, processing, error } = this.props; + + const spinnerBackgroundStyle = { + zIndex: '5', + position: 'fixed', + height: '100%', + width: '100%', + opacity: '0.75', + backgroundColor: 'white' + } + return (
      + { + processing.state && + +
      + +
      +
      + } +
      -

      Create Email Id

      -
      -
      - {this.emailId = c;}} autoFocus="autoFocus" required="required" /> - -
      Email Id must be less than {CONSTANTS.EMAIL_ID_MAX_LENGTH} characters. (This is just a restriction in this tutorial)
      +
      +
      +
      +

      Create Email Id

      + +
      + {this.emailId = c;}} autoFocus="autoFocus" required="required" /> + +
      Email Id must be less than {CONSTANTS.EMAIL_ID_MAX_LENGTH} characters. (This is just a restriction in this tutorial)
      +
      +
      + +
      + +
      -
      - +
      +
      +

      Select Email Id

      +
      +
      +
      + { this.emailSelected = c;}} + readOnly="readOnly" + placeholder="Select email ID" + tabIndex="-1" + value={emailIds[0]} + /> + { + emailIds.length !== 0 ? (
        + { emailIds.map((email, i) => { + return (
      • {email}
      • ) + }) } +
      ) : null + } +
      +
      + +
      +
      +
      +
      - +

      {error.message}

      diff --git a/email_app/app/components/home.js b/email_app/app/components/home.js index 11bf3d5..9a8b845 100755 --- a/email_app/app/components/home.js +++ b/email_app/app/components/home.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { Link, IndexLink } from 'react-router'; -import {ModalDialog, ModalPortal, ModalBackground} from 'react-modal-dialog'; +import { ModalDialog, ModalPortal, ModalBackground } from 'react-modal-dialog'; import ReactSpinner from 'react-spinjs'; import className from 'classnames'; import pkg from '../../package.json'; @@ -15,17 +15,17 @@ export default class Home extends Component { } reconnect() { - const { reconnectApplication, accounts, refreshEmail } = this.props; + const { reconnectApplication, account, refreshEmail } = this.props; return reconnectApplication() - .then(() => refreshEmail(accounts), + .then(() => refreshEmail(account), (err) => 'failed reconnecting'); } render() { const { router } = this.context; - const { coreData, inboxSize, savedSize, network_status, processing } = this.props; + const { coreData, inboxSize, savedSize, networkStatus, processing } = this.props; - const isModalOpen = processing.state || (network_status !== CONSTANTS.NET_STATUS_CONNECTED); + const isModalOpen = processing.state || (networkStatus !== CONSTANTS.NET_STATUS_CONNECTED); const spinnerBackgroundStyle = { zIndex: '5', position: 'fixed', diff --git a/email_app/app/components/initializer.js b/email_app/app/components/initializer.js index a1f2e3c..4cb9244 100644 --- a/email_app/app/components/initializer.js +++ b/email_app/app/components/initializer.js @@ -4,9 +4,9 @@ import { remote } from 'electron'; import { showError } from '../utils/app_utils'; import { MESSAGES, APP_STATUS } from '../constants'; -const showAuthError = (app_status) => { +const showAuthError = (appStatus) => { let message = MESSAGES.AUTHORISATION_ERROR; - if (app_status === APP_STATUS.AUTHORISATION_DENIED) { + if (appStatus === APP_STATUS.AUTHORISATION_DENIED) { message = MESSAGES.AUTHORISATION_DENIED; } showError('Authorisation failed', message, _ => { remote.getCurrentWindow().close(); }); @@ -15,7 +15,7 @@ const showAuthError = (app_status) => { export default class Initializer extends Component { constructor() { super(); - this.refreshConfig = this.refreshConfig.bind(this); + this.readEmailIds = this.readEmailIds.bind(this); } componentDidMount() { @@ -25,31 +25,22 @@ export default class Initializer extends Component { return authoriseApplication(); } - refreshConfig() { - const { setInitializerTask, refreshConfig } = this.props; - setInitializerTask(MESSAGES.INITIALIZE.CHECK_CONFIGURATION); + readEmailIds() { + const { setInitializerTask, getEmailIds } = this.props; + setInitializerTask(MESSAGES.INITIALIZE.FETCH_EMAIL_IDS); - return refreshConfig() - .then((_) => { - if (Object.keys(this.props.accounts).length > 0) { - return this.context.router.push('/home'); - } - showAuthError(); - }) - .catch((_) => { - console.log("No email account found"); - return this.context.router.push('/create_account'); - }); + return getEmailIds() + .then((_) => this.context.router.push('/create_account')); } componentDidUpdate(prevProps, prevState) { - const { app_status, app } = this.props; - if (prevProps.app_status === APP_STATUS.AUTHORISING - && (app_status === APP_STATUS.AUTHORISATION_DENIED - || app_status === APP_STATUS.AUTHORISATION_FAILED) ) { - showAuthError(app_status); - } else if (app && app_status === APP_STATUS.AUTHORISED) { - return this.refreshConfig(); + const { appStatus, app } = this.props; + if (prevProps.appStatus === APP_STATUS.AUTHORISING + && (appStatus === APP_STATUS.AUTHORISATION_DENIED + || appStatus === APP_STATUS.AUTHORISATION_FAILED) ) { + showAuthError(appStatus); + } else if (app && appStatus === APP_STATUS.AUTHORISED) { + return this.readEmailIds(); } } diff --git a/email_app/app/components/mail_inbox.js b/email_app/app/components/mail_inbox.js index b5b9f2e..7704daf 100644 --- a/email_app/app/components/mail_inbox.js +++ b/email_app/app/components/mail_inbox.js @@ -18,11 +18,8 @@ export default class MailInbox extends Component { e.preventDefault(); } - const { refreshEmail, accounts } = this.props; - // TODO: Eventually the app can allow to choose which email account, - // it now supports only one. - let chosenAccount = accounts; - refreshEmail(chosenAccount) + const { refreshEmail, account } = this.props; + refreshEmail(account) .catch((error) => { console.error('Failed fetching emails: ', error); showError('Failed fetching emails: ', error); diff --git a/email_app/app/components/mail_list.js b/email_app/app/components/mail_list.js index 8a92c55..fb68c50 100644 --- a/email_app/app/components/mail_list.js +++ b/email_app/app/components/mail_list.js @@ -25,38 +25,35 @@ export default class MailList extends Component { handleDeleteFromInbox(e) { e.preventDefault(); - const { accounts, deleteEmail, refreshEmail } = this.props; - deleteEmail(accounts.inbox_md, e.target.dataset.index) + const { account, deleteEmail, refreshEmail } = this.props; + deleteEmail(account.inboxMd, e.target.dataset.index) .catch((error) => { console.error('Failed trying to delete email from inbox: ', error); showError('Failed trying to delete email from inbox: ', error); }) - .then(() => this.refreshEmail(accounts)) + .then(() => this.refreshEmail(account)) } handleDeleteSaved(e) { e.preventDefault(); - const { accounts, deleteEmail, refreshEmail } = this.props; - deleteEmail(accounts.archive_md, e.target.dataset.index) + const { account, deleteEmail, refreshEmail } = this.props; + deleteEmail(account.archiveMd, e.target.dataset.index) .catch((error) => { console.error('Failed trying to delete saved email: ', error); showError('Failed trying to delete saved email: ', error); }) - .then(() => this.refreshEmail(accounts)) + .then(() => this.refreshEmail(account)) } handleSave(e) { e.preventDefault(); - const { accounts, saveEmail, refreshEmail } = this.props; - // TODO: Eventually the app can allow to choose which email account, - // it now supports only one. - let chosenAccount = accounts; - saveEmail(chosenAccount, e.target.dataset.index) + const { account, saveEmail, refreshEmail } = this.props; + saveEmail(account, e.target.dataset.index) .catch((error) => { console.error('Failed trying to save the email: ', error); showError('Failed trying to save the email: ', error); }) - .then(() => this.refreshEmail(chosenAccount)) + .then(() => this.refreshEmail(account)) } render() { diff --git a/email_app/app/constants.js b/email_app/app/constants.js index e95230a..9e0697f 100644 --- a/email_app/app/constants.js +++ b/email_app/app/constants.js @@ -4,10 +4,11 @@ export const CONSTANTS = { TAG_TYPE_INBOX: 15003, TAG_TYPE_EMAIL_ARCHIVE: 15004, SERVICE_NAME_POSTFIX: "@email", - MD_KEY_EMAIL_INBOX: "email_inbox", - MD_KEY_EMAIL_ARCHIVE: "email_archive", - MD_KEY_EMAIL_ID: "email_id", - MD_KEY_EMAIL_ENC_SECRET_KEY: "__email_enc_sk", + ACCOUNT_KEY_EMAIL_INBOX: "inbox", + ACCOUNT_KEY_EMAIL_ARCHIVE: "archive", + ACCOUNT_KEY_EMAIL_ID: "email_id", + ACCOUNT_KEY_EMAIL_ENC_SECRET_KEY: "email_enc_sk", + ACCOUNT_KEY_EMAIL_ENC_PUBLIC_KEY: "email_enc_pk", MD_KEY_EMAIL_ENC_PUBLIC_KEY: "__email_enc_pk", TOTAL_INBOX_SIZE: 100, EMAIL_ID_MAX_LENGTH: 100, @@ -25,14 +26,17 @@ export const APP_STATUS = { AUTHORISATION_FAILED: 'AUTHORISATION_FAILED', AUTHORISATION_DENIED: 'AUTHORISATION_DENIED', AUTHORISED: 'AUTHORISED', + FETCHING_EMAIL_IDS: 'FETCHING_EMAIL_IDS', READING_CONFIG: 'READING_CONFIG', READY: 'READY' } export const SAFE_APP_ERROR_CODES = { ERR_AUTH_DENIED: -200, + ENTRY_ALREADY_EXISTS: -107, ERR_NO_SUCH_ENTRY: -106, ERR_DATA_EXISTS: -104, + ERR_DATA_NOT_FOUND: -103, ERR_OPERATION_ABORTED: -14 } @@ -40,9 +44,7 @@ export const MESSAGES = { INITIALIZE: { AUTHORISE_APP: 'Authorising Application', CHECK_CONFIGURATION: 'Checking configuration', - FETCH_CORE_STRUCTURE: 'Fetching Core Structure', - CREATE_CORE_STRUCTURE: 'Creating Core Structure', - WRITE_CONFIG_FILE: 'Creating new configuration', + FETCH_EMAIL_IDS: 'Fetching owned email Ids' }, EMAIL_ALREADY_TAKEN: 'Email ID already taken. Please try again', EMAIL_ID_TOO_LONG: 'Email ID is too long', diff --git a/email_app/app/containers/compose_mail_container.js b/email_app/app/containers/compose_mail_container.js index 4430b14..f49d36c 100644 --- a/email_app/app/containers/compose_mail_container.js +++ b/email_app/app/containers/compose_mail_container.js @@ -13,7 +13,7 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { sendEmail: (email, to) => (dispatch(sendEmail(email, to))), - cancelCompose: _ => dispatch(cancelCompose()) + cancelCompose: (_) => dispatch(cancelCompose()) }; }; diff --git a/email_app/app/containers/create_account_container.js b/email_app/app/containers/create_account_container.js index bb74fd6..9036243 100644 --- a/email_app/app/containers/create_account_container.js +++ b/email_app/app/containers/create_account_container.js @@ -1,12 +1,13 @@ import { connect } from 'react-redux'; import CreateAccount from '../components/create_account'; import { createAccount, createAccountError } from '../actions/create_account_actions'; -import { storeNewAccount } from '../actions/initializer_actions'; +import { storeNewAccount, refreshConfig } from '../actions/initializer_actions'; const mapStateToProps = state => { return { error: state.createAccount.error, processing: state.initializer.processing, + emailIds: state.initializer.emailIds, newAccount: state.createAccount.newAccount, coreData: state.initializer.coreData }; @@ -14,9 +15,10 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - createAccountError: error => (dispatch(createAccountError(error))), - createAccount: emailId => (dispatch(createAccount(emailId))), - storeNewAccount: account => (dispatch(storeNewAccount(account))) + createAccountError: (error) => (dispatch(createAccountError(error))), + createAccount: (emailId) => (dispatch(createAccount(emailId))), + storeNewAccount: (account) => (dispatch(storeNewAccount(account))), + refreshConfig: (emailId) => (dispatch(refreshConfig(emailId))) }; }; diff --git a/email_app/app/containers/home_container.js b/email_app/app/containers/home_container.js index 4644919..98fc2d1 100644 --- a/email_app/app/containers/home_container.js +++ b/email_app/app/containers/home_container.js @@ -5,10 +5,10 @@ import { reconnectApplication, refreshEmail } from '../actions/initializer_actio const mapStateToProps = state => { return { coreData: state.initializer.coreData, - accounts: state.initializer.accounts, + account: state.initializer.account, inboxSize: state.initializer.inboxSize, savedSize: state.initializer.savedSize, - network_status: state.initializer.network_status, + networkStatus: state.initializer.networkStatus, processing: state.initializer.processing }; }; diff --git a/email_app/app/containers/initializer_container.js b/email_app/app/containers/initializer_container.js index 7e7571c..ee2f0fa 100644 --- a/email_app/app/containers/initializer_container.js +++ b/email_app/app/containers/initializer_container.js @@ -1,24 +1,20 @@ import { connect } from 'react-redux'; import Initializer from '../components/initializer'; -import { setInitializerTask, authoriseApplication, - refreshConfig, refreshEmail } from '../actions/initializer_actions'; +import { setInitializerTask, authoriseApplication, getEmailIds } from '../actions/initializer_actions'; const mapStateToProps = state => { return { - app_status: state.initializer.app_status, + appStatus: state.initializer.appStatus, app: state.initializer.app, - accounts: state.initializer.accounts, - tasks: state.initializer.tasks, - coreData: state.initializer.coreData + tasks: state.initializer.tasks }; }; const mapDispatchToProps = dispatch => { return { - setInitializerTask: task => (dispatch(setInitializerTask(task))), + setInitializerTask: (task) => (dispatch(setInitializerTask(task))), authoriseApplication: () => (dispatch(authoriseApplication())), - refreshConfig: () => (dispatch(refreshConfig())), - refreshEmail: (account) => (dispatch(refreshEmail(account))) + getEmailIds: () => (dispatch(getEmailIds())) }; }; diff --git a/email_app/app/containers/mail_inbox_container.js b/email_app/app/containers/mail_inbox_container.js index 9f19423..3bb6c47 100644 --- a/email_app/app/containers/mail_inbox_container.js +++ b/email_app/app/containers/mail_inbox_container.js @@ -10,7 +10,7 @@ const mapStateToProps = state => { inboxSize: state.initializer.inboxSize, savedSize: state.initializer.savedSize, app: state.initializer.app, - accounts: state.initializer.accounts + account: state.initializer.account }; }; diff --git a/email_app/app/containers/mail_saved_container.js b/email_app/app/containers/mail_saved_container.js index 973a4c9..42b056d 100644 --- a/email_app/app/containers/mail_saved_container.js +++ b/email_app/app/containers/mail_saved_container.js @@ -9,7 +9,7 @@ const mapStateToProps = state => { processing: state.mail.processing, error: state.mail.error, app: state.initializer.app, - accounts: state.initializer.accounts + account: state.initializer.account }; }; diff --git a/email_app/app/less/authenticate.less b/email_app/app/less/authenticate.less index bfa7ef1..b1de004 100644 --- a/email_app/app/less/authenticate.less +++ b/email_app/app/less/authenticate.less @@ -1,26 +1,57 @@ @import "variables.less"; .create-account { - width: 100%; - height: 100%; - display: table; - .create-account-b { - display: table-cell; - vertical-align: middle; - .create-account-cnt { - width: 300px; - margin: 0 auto; - .error { - color: @red-100; - } - .title { - font-size: 36px; - font-weight: normal; - opacity: 0.8; - } - .form { - margin-top: 80px; - } - } - } + width: 100%; + height: 100%; + display: table; + .create-account-b { + display: table-cell; + vertical-align: middle; + .create-account-cnt { + width: 100%; + margin: 0 auto; + .error { + margin-top: 40px; + font-size: 16px; + font-weight: normal; + color: @red-100; + } + .title { + font-size: 20px; + font-weight: 300; + opacity: 0.8; + } + .email-ls { + .mdl-textfield { + width: 100%; + margin-top: -20px; + font-family: 'Open Sans'; + } + .mdl-textfield__label { + color: #000; + font-size: 14px; + top: -11px; + font-weight: 600; + } + .form .inp-btn-cnt { + margin-top: 70px; + } + .form .inp-grp { + margin-bottom: 0; + } + } + } + } } + +.split-view { + width: 70%; + margin: 0 auto; + .split-view-i { + width: 50%; + display: inline-block; + float: left; + padding: 30px; + } + .clearfix(); +} \ No newline at end of file diff --git a/email_app/app/reducers/initialiser.js b/email_app/app/reducers/initialiser.js index 6c3b6b4..12ecc37 100644 --- a/email_app/app/reducers/initialiser.js +++ b/email_app/app/reducers/initialiser.js @@ -2,15 +2,16 @@ import ACTION_TYPES from '../actions/actionTypes'; import { MESSAGES, APP_STATUS, CONSTANTS, SAFE_APP_ERROR_CODES } from '../constants'; const initialState = { - app_status: null, - network_status: null, + appStatus: null, + networkStatus: null, processing: { state: false, msg: null }, app: null, tasks: [], - accounts: [], + emailIds: [], + account: [], coreData: { id: '', inbox: [], @@ -29,13 +30,13 @@ const initializer = (state = initialState, action) => { break; } case `${ACTION_TYPES.AUTHORISE_APP}_LOADING`: - return { ...state, app: null, app_status: APP_STATUS.AUTHORISING }; + return { ...state, app: null, appStatus: APP_STATUS.AUTHORISING }; break; case `${ACTION_TYPES.AUTHORISE_APP}_SUCCESS`: return { ...state, app: action.payload, - app_status: APP_STATUS.AUTHORISED, - network_status: CONSTANTS.NET_STATUS_CONNECTED + appStatus: APP_STATUS.AUTHORISED, + networkStatus: CONSTANTS.NET_STATUS_CONNECTED }; break; case `${ACTION_TYPES.AUTHORISE_APP}_ERROR`: @@ -43,10 +44,10 @@ const initializer = (state = initialState, action) => { if (action.payload.code === SAFE_APP_ERROR_CODES.ERR_AUTH_DENIED) { status = APP_STATUS.AUTHORISATION_DENIED; } - return { ...state, app_status: status }; + return { ...state, appStatus: status }; break; case ACTION_TYPES.NET_STATUS_CHANGED: - return { ...state, network_status: action.payload }; + return { ...state, networkStatus: action.payload }; break; case `${ACTION_TYPES.RECONNECT_APP}_LOADING`: return { ...state, processing: { state: true, msg: 'Reconnecting...' } }; @@ -56,18 +57,28 @@ const initializer = (state = initialState, action) => { break; case `${ACTION_TYPES.RECONNECT_APP}_SUCCESS`: return { ...state, - network_status: CONSTANTS.NET_STATUS_CONNECTED, + networkStatus: CONSTANTS.NET_STATUS_CONNECTED, processing: { state: false, msg: null } }; break; + case `${ACTION_TYPES.FETCH_EMAIL_IDS}_LOADING`: + return { ...state, appStatus: APP_STATUS.FETCHING_EMAIL_IDS }; + break; + case `${ACTION_TYPES.FETCH_EMAIL_IDS}_SUCCESS`: + return { ...state, emailIds: action.payload }; + break; case `${ACTION_TYPES.GET_CONFIG}_LOADING`: - return { ...state, app_status: APP_STATUS.READING_CONFIG }; + return { ...state, + appStatus: APP_STATUS.READING_CONFIG, + processing: { state: true, msg: 'Reading emails...' } + }; break; case `${ACTION_TYPES.GET_CONFIG}_SUCCESS`: return { ...state, - accounts: action.payload, + account: action.payload, coreData: { ...state.coreData, id: action.payload.id }, - app_status: APP_STATUS.READY + appStatus: APP_STATUS.READY, + processing: { state: false, msg: null } }; break; case `${ACTION_TYPES.CREATE_ACCOUNT}_LOADING`: @@ -82,7 +93,7 @@ const initializer = (state = initialState, action) => { break; case `${ACTION_TYPES.STORE_NEW_ACCOUNT}_SUCCESS`: return { ...state, - accounts: action.payload, + account: action.payload, coreData: { ...state.coreData, id: action.payload.id }, processing: { state: false, msg: null } }; diff --git a/email_app/app/safenet_comm.js b/email_app/app/safenet_comm.js index 299cb81..9d11a0f 100644 --- a/email_app/app/safenet_comm.js +++ b/email_app/app/safenet_comm.js @@ -1,8 +1,8 @@ import { shell } from 'electron'; import { CONSTANTS, MESSAGES, SAFE_APP_ERROR_CODES } from './constants'; import { initializeApp, fromAuthURI } from 'safe-app'; -import { getAuthData, saveAuthData, clearAuthData, hashPublicId, genRandomEntryKey, - genKeyPair, encrypt, decrypt, genServiceInfo, deserialiseArray, parseUrl } from './utils/app_utils'; +import { getAuthData, saveAuthData, clearAuthData, genRandomEntryKey, + genKeyPair, encrypt, decrypt, splitPublicIdAndService, deserialiseArray, parseUrl } from './utils/app_utils'; import pkg from '../package.json'; const APP_INFO = { @@ -23,6 +23,15 @@ const APP_INFO = { } }; +const genServiceInfo = (app, emailId) => { + let serviceInfo = splitPublicIdAndService(emailId); + return app.crypto.sha3Hash(serviceInfo.publicId) + .then((hashed) => { + serviceInfo.serviceAddr = hashed; + return serviceInfo; + }); +} + const requestAuth = () => { return initializeApp(APP_INFO.info) .then((app) => app.auth.genAuthUri(APP_INFO.permissions, APP_INFO.opts) @@ -42,8 +51,8 @@ export const authApp = (netStatusCallback) => { let uri = getAuthData(); if (uri) { return fromAuthURI(APP_INFO.info, uri, netStatusCallback) - .then((registered_app) => registered_app.auth.refreshContainersPermissions() - .then(() => registered_app) + .then((registeredApp) => registeredApp.auth.refreshContainersPermissions() + .then(() => registeredApp) ) .catch((err) => { console.warn("Auth URI stored is not valid anymore, app needs to be re-authorised."); @@ -56,58 +65,106 @@ export const authApp = (netStatusCallback) => { } export const connect = (uri, netStatusCallback) => { - let registered_app; + let registeredApp; return fromAuthURI(APP_INFO.info, uri, netStatusCallback) - .then((app) => registered_app = app) + .then((app) => registeredApp = app) .then(() => saveAuthData(uri)) - .then(() => registered_app.auth.refreshContainersPermissions()) - .then(() => registered_app); + .then(() => registeredApp.auth.refreshContainersPermissions()) + .then(() => registeredApp); } export const reconnect = (app) => { return app.reconnect(); } -export const readConfig = (app) => { - let account = {}; +const fetchPublicIds = (app) => { + let rawEntries = []; + let publicIds = []; + return app.auth.getContainer(APP_INFO.containers.publicNames) + .then((pubNamesMd) => pubNamesMd.getEntries() + .then((entries) => entries.forEach((key, value) => { + rawEntries.push({key, value}); + }) + .then(() => Promise.all(rawEntries.map((entry) => { + if (entry.value.buf.length === 0) { //FIXME: this condition is a work around for a limitation in safe_core + return Promise.resolve(); + } + + return pubNamesMd.decrypt(entry.key) + .then((decKey) => pubNamesMd.decrypt(entry.value.buf) + .then((decVal) => publicIds.push({ + id: decKey.toString(), + service: decVal + }) + )); + }))) + )) + .then(() => publicIds); +} + +export const fetchEmailIds = (app) => { + let emailIds = []; + + return fetchPublicIds(app) + .then((publicIds) => Promise.all(publicIds.map((publicId) => { + let rawEmailIds = []; + return app.mutableData.newPublic(publicId.service, CONSTANTS.TAG_TYPE_DNS) + .then((servicesMd) => servicesMd.getKeys()) + .then((keys) => keys.forEach((key) => { + rawEmailIds.push(key.toString()); + }) + .then(() => Promise.all(rawEmailIds.map((emailId) => { + // Let's filter out the services which are not email services, + // i.e. those which don't have the `@email` postfix + const regex = new RegExp('.*(?=' + CONSTANTS.SERVICE_NAME_POSTFIX +'$)', 'g'); + let res = regex.exec(emailId); + if (res) { + emailIds.push(res[0] + ((res[0].length > 0) ? '.' : '') + publicId.id); + } + }))) + ); + }))) + .then(() => emailIds); +} + +export const readConfig = (app, emailId) => { + let account = {id: emailId}; + let storedAccount = {} + return app.auth.getHomeContainer() - .then((md) => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_INBOX).then((key) => md.get(key)) - .then((value) => md.decrypt(value.buf).then((decrypted) => app.mutableData.fromSerial(decrypted))) - .then((inbox_md) => account.inbox_md = inbox_md) - .then(() => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_ARCHIVE).then((key) => md.get(key))) - .then((value) => md.decrypt(value.buf).then((decrypted) => app.mutableData.fromSerial(decrypted))) - .then((archive_md) => account.archive_md = archive_md) - .then(() => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_ID).then((key) => md.get(key))) - .then((value) => md.decrypt(value.buf).then((decrypted) => account.id = decrypted.toString())) - .then(() => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_ENC_SECRET_KEY).then((key) => md.get(key))) - .then((value) => md.decrypt(value.buf).then((decrypted) => account.enc_sk = decrypted.toString())) - .then(() => md.encryptKey(CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY).then((key) => md.get(key))) - .then((value) => md.decrypt(value.buf).then((decrypted) => account.enc_pk = decrypted.toString())) + .then((md) => md.encryptKey(emailId).then((key) => md.get(key)) + .then((value) => md.decrypt(value.buf).then((decrypted) => storedAccount = JSON.parse(decrypted))) + .then(() => app.mutableData.fromSerial(storedAccount[CONSTANTS.ACCOUNT_KEY_EMAIL_INBOX])) + .then((inboxMd) => account.inboxMd = inboxMd) + .then(() => app.mutableData.fromSerial(storedAccount[CONSTANTS.ACCOUNT_KEY_EMAIL_ARCHIVE])) + .then((archiveMd) => account.archiveMd = archiveMd) + .then(() => account.encSk = storedAccount[CONSTANTS.ACCOUNT_KEY_EMAIL_ENC_SECRET_KEY]) + .then(() => account.encPk = storedAccount[CONSTANTS.ACCOUNT_KEY_EMAIL_ENC_PUBLIC_KEY]) ) .then(() => account); } const insertEncrypted = (md, mut, key, value) => { return md.encryptKey(key) - .then((encrypted_key) => md.encryptValue(value) - .then((encrypted_value) => mut.insert(encrypted_key, encrypted_value)) + .then((encryptedKey) => md.encryptValue(value) + .then((encryptedValue) => mut.insert(encryptedKey, encryptedValue)) ); } export const writeConfig = (app, account) => { - let serialised_inbox; - let serialised_archive; - return account.inbox_md.serialise() - .then((serial) => serialised_inbox = serial) - .then(() => account.archive_md.serialise()) - .then((serial) => serialised_archive = serial) + let emailAccount = { + [CONSTANTS.ACCOUNT_KEY_EMAIL_ID]: account.id, + [CONSTANTS.ACCOUNT_KEY_EMAIL_ENC_SECRET_KEY]: account.encSk, + [CONSTANTS.ACCOUNT_KEY_EMAIL_ENC_PUBLIC_KEY]: account.encPk + }; + + return account.inboxMd.serialise() + .then((serial) => emailAccount[CONSTANTS.ACCOUNT_KEY_EMAIL_INBOX] = serial) + .then(() => account.archiveMd.serialise()) + .then((serial) => emailAccount[CONSTANTS.ACCOUNT_KEY_EMAIL_ARCHIVE] = serial) .then(() => app.auth.getHomeContainer()) .then((md) => app.mutableData.newMutation() - .then((mut) => insertEncrypted(md, mut, CONSTANTS.MD_KEY_EMAIL_INBOX, serialised_inbox) - .then(() => insertEncrypted(md, mut, CONSTANTS.MD_KEY_EMAIL_ARCHIVE, serialised_archive)) - .then(() => insertEncrypted(md, mut, CONSTANTS.MD_KEY_EMAIL_ID, account.id)) - .then(() => insertEncrypted(md, mut, CONSTANTS.MD_KEY_EMAIL_ENC_SECRET_KEY, account.enc_sk)) - .then(() => insertEncrypted(md, mut, CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY, account.enc_pk)) + .then((mut) => insertEncrypted(md, mut, account.id, JSON.stringify(emailAccount)) .then(() => md.applyEntriesMutation(mut)) )) .then(() => account); @@ -115,19 +172,19 @@ export const writeConfig = (app, account) => { const decryptEmail = (app, account, key, value, cb) => { if (value.length > 0) { //FIXME: this condition is a work around for a limitation in safe_core - let entry_value = decrypt(value, account.enc_sk, account.enc_pk); - return app.immutableData.fetch(deserialiseArray(entry_value)) + let entryValue = decrypt(value, account.encSk, account.encPk); + return app.immutableData.fetch(deserialiseArray(entryValue)) .then((immData) => immData.read()) .then((content) => { let decryptedEmail; - decryptedEmail = JSON.parse(decrypt(content.toString(), account.enc_sk, account.enc_pk)); + decryptedEmail = JSON.parse(decrypt(content.toString(), account.encSk, account.encPk)); cb({ [key]: decryptedEmail }); }); } } export const readInboxEmails = (app, account, cb) => { - return account.inbox_md.getEntries() + return account.inboxMd.getEntries() .then((entries) => entries.forEach((key, value) => { if (key.toString() !== CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY) { return decryptEmail(app, account, key, value.buf.toString(), cb); @@ -137,28 +194,28 @@ export const readInboxEmails = (app, account, cb) => { } export const readArchivedEmails = (app, account, cb) => { - return account.archive_md.getEntries() + return account.archiveMd.getEntries() .then((entries) => entries.forEach((key, value) => { return decryptEmail(app, account, key, value.buf.toString(), cb); }) ); } -const createInbox = (app, enc_pk) => { - let base_inbox = { - [CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY]: enc_pk +const createInbox = (app, encPk) => { + let baseInbox = { + [CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY]: encPk }; - let inbox_md; + let inboxMd; let permSet; return app.mutableData.newRandomPublic(CONSTANTS.TAG_TYPE_INBOX) - .then((md) => md.quickSetup(base_inbox)) - .then((md) => inbox_md = md) + .then((md) => md.quickSetup(baseInbox)) + .then((md) => inboxMd = md) .then(() => app.mutableData.newPermissionSet()) .then((pmSet) => permSet = pmSet) .then(() => permSet.setAllow('Insert')) - .then(() => inbox_md.setUserPermissions(null, permSet, 1)) - .then(() => inbox_md); + .then(() => inboxMd.setUserPermissions(null, permSet, 1)) + .then(() => inboxMd); } const createArchive = (app) => { @@ -166,42 +223,50 @@ const createArchive = (app) => { .then((md) => md.quickSetup()); } -const addEmailService = (app, serviceInfo, inbox_serialised) => { - return app.mutableData.newPublic(serviceInfo.serviceAddr, CONSTANTS.TAG_TYPE_DNS) - .then((md) => md.quickSetup({ [serviceInfo.serviceName]: inbox_serialised })) +const addEmailService = (app, servicesXorName, serviceName, inboxSerialised) => { + return app.mutableData.newPublic(servicesXorName, CONSTANTS.TAG_TYPE_DNS) + .then((md) => app.mutableData.newMutation() + .then((mut) => mut.insert(serviceName, inboxSerialised) + .then(() => md.applyEntriesMutation(mut)) + ) + .then(() => md)); } -const createPublicIdAndEmailService = (app, pub_names_md, serviceInfo, - inbox_serialised) => { - return addEmailService(app, serviceInfo, inbox_serialised) - .then((md) => md.getNameAndTag()) - .then((services) => app.mutableData.newMutation() - .then((mut) => insertEncrypted(pub_names_md, mut, serviceInfo.publicId, services.name) - .then(() => pub_names_md.applyEntriesMutation(mut)) - )) +const createPublicIdAndEmailService = (app, pubNamesMd, serviceInfo, + inboxSerialised) => { + return app.mutableData.newPublic(serviceInfo.serviceAddr, CONSTANTS.TAG_TYPE_DNS) + .then((md) => md.quickSetup({ [serviceInfo.serviceName]: inboxSerialised })) + .then((_) => app.mutableData.newMutation() + .then((mut) => insertEncrypted(pubNamesMd, mut, serviceInfo.publicId, serviceInfo.serviceAddr) + .then(() => pubNamesMd.applyEntriesMutation(mut)) + )); } export const setupAccount = (app, emailId) => { let newAccount = {}; - let inbox_serialised; + let inboxSerialised; let inbox; - let key_pair = genKeyPair(); - let serviceInfo = genServiceInfo(emailId); + let serviceInfo; + let keyPair = genKeyPair(); - return createInbox(app, key_pair.publicKey) + return genServiceInfo(app, emailId) + .then((info) => serviceInfo = info) + .then(() => createInbox(app, keyPair.publicKey)) .then((md) => inbox = md) .then(() => createArchive(app)) - .then((md) => newAccount = {id: serviceInfo.emailId, inbox_md: inbox, archive_md: md, - enc_sk: key_pair.privateKey, enc_pk: key_pair.publicKey}) - .then(() => newAccount.inbox_md.serialise()) - .then((md_serialised) => inbox_serialised = md_serialised) + .then((md) => newAccount = {id: serviceInfo.emailId, inboxMd: inbox, archiveMd: md, + encSk: keyPair.privateKey, encPk: keyPair.publicKey}) + .then(() => newAccount.inboxMd.serialise()) + .then((mdSerialised) => inboxSerialised = mdSerialised) .then(() => app.auth.getContainer(APP_INFO.containers.publicNames)) - .then((pub_names_md) => pub_names_md.encryptKey(serviceInfo.publicId).then((key) => pub_names_md.get(key)) - .then((services) => addEmailService(app, serviceInfo, inbox_serialised) + .then((pubNamesMd) => pubNamesMd.encryptKey(serviceInfo.publicId).then((key) => pubNamesMd.get(key)) + .then((encryptedAddr) => pubNamesMd.decrypt(encryptedAddr.buf) + .then((servicesXorName) => addEmailService(app, servicesXorName, + serviceInfo.serviceName, inboxSerialised)) , (err) => { if (err.code === SAFE_APP_ERROR_CODES.ERR_NO_SUCH_ENTRY) { - return createPublicIdAndEmailService(app, pub_names_md, - serviceInfo, inbox_serialised); + return createPublicIdAndEmailService(app, pubNamesMd, + serviceInfo, inboxSerialised); } throw err; }) @@ -220,19 +285,21 @@ const writeEmailContent = (app, email, pk) => { } export const storeEmail = (app, email, to) => { - let serviceInfo = genServiceInfo(to); - return app.mutableData.newPublic(serviceInfo.serviceAddr, CONSTANTS.TAG_TYPE_DNS) + let serviceInfo; + return genServiceInfo(app, to) + .then((info) => serviceInfo = info) + .then(() => app.mutableData.newPublic(serviceInfo.serviceAddr, CONSTANTS.TAG_TYPE_DNS)) .then((md) => md.get(serviceInfo.serviceName)) .catch((err) => {throw MESSAGES.EMAIL_ID_NOT_FOUND}) .then((service) => app.mutableData.fromSerial(service.buf)) - .then((inbox_md) => inbox_md.get(CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY) + .then((inboxMd) => inboxMd.get(CONSTANTS.MD_KEY_EMAIL_ENC_PUBLIC_KEY) .then((pk) => writeEmailContent(app, email, pk.buf.toString()) - .then((email_addr) => app.mutableData.newMutation() + .then((emailAddr) => app.mutableData.newMutation() .then((mut) => { - let entry_key = genRandomEntryKey(); - let entry_value = encrypt(email_addr.toString(), pk.buf.toString()); - return mut.insert(entry_key, entry_value) - .then(() => inbox_md.applyEntriesMutation(mut)) + let entryKey = genRandomEntryKey(); + let entryValue = encrypt(emailAddr.toString(), pk.buf.toString()); + return mut.insert(entryKey, entryValue) + .then(() => inboxMd.applyEntriesMutation(mut)) }) ))); } @@ -245,15 +312,15 @@ export const removeEmail = (app, container, key) => { } export const archiveEmail = (app, account, key) => { - let new_entry_key = genRandomEntryKey(); - return account.inbox_md.get(key) + let newEntryKey = genRandomEntryKey(); + return account.inboxMd.get(key) .then((xorName) => app.mutableData.newMutation() - .then((mut) => mut.insert(new_entry_key, xorName.buf) - .then(() => account.archive_md.applyEntriesMutation(mut)) + .then((mut) => mut.insert(newEntryKey, xorName.buf) + .then(() => account.archiveMd.applyEntriesMutation(mut)) ) ) .then(() => app.mutableData.newMutation()) .then((mut) => mut.remove(key, 1) - .then(() => account.inbox_md.applyEntriesMutation(mut)) + .then(() => account.inboxMd.applyEntriesMutation(mut)) ) } diff --git a/email_app/app/utils/app_utils.js b/email_app/app/utils/app_utils.js index e7a67eb..4199ec4 100644 --- a/email_app/app/utils/app_utils.js +++ b/email_app/app/utils/app_utils.js @@ -23,7 +23,7 @@ export const clearAuthData = () => { // window.localStorage.removeItem(CONSTANTS.LOCAL_AUTH_DATA_KEY); }; -export const genServiceInfo = (emailId) => { +export const splitPublicIdAndService = (emailId) => { // It supports complex email IDs, e.g. 'emailA.myshop', 'emailB.myshop' let str = emailId.replace(/\.+$/, ''); let toParts = str.split('.'); @@ -31,14 +31,9 @@ export const genServiceInfo = (emailId) => { const serviceId = str.slice(0, -1 * (publicId.length+1)); emailId = (serviceId.length > 0 ? (serviceId + '.') : '') + publicId; const serviceName = serviceId + CONSTANTS.SERVICE_NAME_POSTFIX; - const serviceAddr = hashPublicId(publicId); - return {emailId, publicId, serviceAddr, serviceName}; + return {emailId, publicId, serviceName}; } -export const hashPublicId = publicId => { - return crypto.createHash('sha256').update(publicId).digest(); -}; - export const genRandomEntryKey = () => { return crypto.randomBytes(32).toString('hex'); }; diff --git a/email_app/design/EmailApp-DataModel.png b/email_app/design/EmailApp-DataModel.png index 708c3b605b94afe3b5cd87714647cc1749e7d94f..3a98b2193e5f902046682d91f75502491fa63b61 100644 GIT binary patch literal 95061 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

      h20-i3|NUp2105V$MuwVY15&&*f9fR^A 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 i=d#Wzp$PybK}$#g diff --git a/email_app/design/EmailApp-DataModel.xml b/email_app/design/EmailApp-DataModel.xml index 4cb1627..d72aad8 100644 --- a/email_app/design/EmailApp-DataModel.xml +++ b/email_app/design/EmailApp-DataModel.xml @@ -1 +1 @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/email_app/package.json b/email_app/package.json index 5d93271..85a8fdf 100644 --- a/email_app/package.json +++ b/email_app/package.json @@ -106,6 +106,7 @@ "dateformat": "^1.0.12", "electron-compile": "^6.2.0", "electron-debug": "^1.0.1", + "getmdl-select": "^1.0.4", "less": "^2.7.2", "libsodium-wrappers": "^0.5.1", "material-design-lite": "^1.2.1", From d5724b71bf14449455a1e084d60e4fb1ec43a3fe Mon Sep 17 00:00:00 2001 From: Shankar Date: Wed, 12 Jul 2017 14:34:22 +0530 Subject: [PATCH 42/44] FIX :: Dropdown for email list (#204) * fix/dropDown: resolve dropdown for email list Resolved dropdown for email list * fix/patch_version: update version Updated patch version --- email_app/app/app.html | 2 +- email_app/app/app.js | 1 - email_app/app/components/create_account.js | 47 ++++++++-------------- email_app/app/less/authenticate.less | 25 +++++------- email_app/package.json | 4 +- 5 files changed, 29 insertions(+), 50 deletions(-) diff --git a/email_app/app/app.html b/email_app/app/app.html index ce19cbe..d358457 100755 --- a/email_app/app/app.html +++ b/email_app/app/app.html @@ -8,8 +8,8 @@ - +

      diff --git a/email_app/app/app.js b/email_app/app/app.js index 6ee9b8d..d98905c 100644 --- a/email_app/app/app.js +++ b/email_app/app/app.js @@ -7,7 +7,6 @@ import { ipcRenderer as ipc } from 'electron'; import routes from './routes'; import configureStore from './store/configureStore'; import { receiveResponse, onAuthFailure } from "./actions/initializer_actions"; - const store = configureStore(); const history = syncHistoryWithStore(hashHistory, store); diff --git a/email_app/app/components/create_account.js b/email_app/app/components/create_account.js index 74804b2..abe99c2 100644 --- a/email_app/app/components/create_account.js +++ b/email_app/app/components/create_account.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import ReactMaterialSelect from 'react-material-select' import { MESSAGES, CONSTANTS, SAFE_APP_ERROR_CODES } from '../constants'; import { ModalPortal } from 'react-modal-dialog'; import ReactSpinner from 'react-spinjs'; @@ -46,7 +47,8 @@ export default class CreateAccount extends Component { handleChooseAccount(e) { e.preventDefault(); const { refreshConfig, createAccountError } = this.props; - const emailId = this.emailSelected.value; + const emailId = this.refs.emailSelected.getValue(); + console.log('emailId', emailId) return refreshConfig(emailId) .then((_) => this.context.router.push('/home')) @@ -98,35 +100,20 @@ export default class CreateAccount extends Component {

      Select Email Id

      -
      -
      - { this.emailSelected = c;}} - readOnly="readOnly" - placeholder="Select email ID" - tabIndex="-1" - value={emailIds[0]} - /> - { - emailIds.length !== 0 ? (
        - { emailIds.map((email, i) => { - return (
      • {email}
      • ) - }) } -
      ) : null - } -
      -
      - -
      + + { + emailIds.map((email, i) => { + return () + }) + } + +
      +
      diff --git a/email_app/app/less/authenticate.less b/email_app/app/less/authenticate.less index b1de004..1edc37d 100644 --- a/email_app/app/less/authenticate.less +++ b/email_app/app/less/authenticate.less @@ -22,22 +22,15 @@ opacity: 0.8; } .email-ls { - .mdl-textfield { - width: 100%; - margin-top: -20px; - font-family: 'Open Sans'; - } - .mdl-textfield__label { - color: #000; - font-size: 14px; - top: -11px; - font-weight: 600; - } - .form .inp-btn-cnt { - margin-top: 70px; - } - .form .inp-grp { - margin-bottom: 0; + .rms-wrapper { + margin: 20px 0 70px; + .rms-label { + visibility: hidden; + } + .rms-text span { + border-width: 2px; + border-bottom-color: #6FAFEF; + } } } } diff --git a/email_app/package.json b/email_app/package.json index 85a8fdf..cfb16a2 100644 --- a/email_app/package.json +++ b/email_app/package.json @@ -1,7 +1,7 @@ { "name": "safe-mail-tutorial", "productName": "SAFE-Mail-Tutorial", - "version": "0.2.0", + "version": "0.2.1", "description": "Mailing application tutorial using SAFE Network", "identifier": "net.maidsafe.examples.mailtutorial", "vendor": "MaidSafe", @@ -106,7 +106,6 @@ "dateformat": "^1.0.12", "electron-compile": "^6.2.0", "electron-debug": "^1.0.1", - "getmdl-select": "^1.0.4", "less": "^2.7.2", "libsodium-wrappers": "^0.5.1", "material-design-lite": "^1.2.1", @@ -115,6 +114,7 @@ "react": "^15.4.2", "react-dom": "^15.4.2", "react-hot-loader": "^3.0.0-beta.6", + "react-material-select": "^1.2.0", "react-modal-dialog": "^4.0.7", "react-redux": "^5.0.3", "react-router": "^3.0.2", From 05dfd78a1efa88215aaedce7124d6300ce485218 Mon Sep 17 00:00:00 2001 From: Shankar Date: Wed, 12 Jul 2017 15:00:18 +0530 Subject: [PATCH 43/44] Update hosting app title (#205) * fix/typo: update hosting app title Updated hosting app title and patch version * fix/typo: update public id typo Updated public id to Public ID --- web_hosting_manager/app/app.html | 2 +- web_hosting_manager/app/locales/en.js | 8 ++++---- web_hosting_manager/package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web_hosting_manager/app/app.html b/web_hosting_manager/app/app.html index 3cf0053..450821b 100644 --- a/web_hosting_manager/app/app.html +++ b/web_hosting_manager/app/app.html @@ -2,7 +2,7 @@ - SAFE Hosting Manager + Web Hosting Manager