From 073393af311d36c8ca7570ff0d3f0a3117c0b544 Mon Sep 17 00:00:00 2001 From: elijah Date: Fri, 16 Sep 2016 14:02:32 -0700 Subject: [pkg] rename www to ui --- .gitignore | 6 +- Makefile | 2 +- README.rst | 4 +- src/leap/bitmask/core/web/README | 6 +- src/leap/bitmask/gui/README.rst | 2 +- ui/Makefile | 47 ++++ ui/README.md | 112 ++++++++ ui/app/app.js | 33 +++ ui/app/components/area.js | 65 +++++ ui/app/components/center.js | 39 +++ ui/app/components/debug_panel.js | 40 +++ ui/app/components/error_panel.js | 21 ++ ui/app/components/greeter_panel.js | 34 +++ ui/app/components/list_edit.js | 122 ++++++++ ui/app/components/login.js | 302 ++++++++++++++++++++ ui/app/components/main_panel/account_list.js | 113 ++++++++ ui/app/components/main_panel/email_section.js | 48 ++++ ui/app/components/main_panel/index.js | 80 ++++++ ui/app/components/main_panel/main_panel.less | 212 ++++++++++++++ ui/app/components/main_panel/section_layout.js | 59 ++++ ui/app/components/main_panel/user_section.js | 71 +++++ ui/app/components/main_panel/vpn_section.js | 0 ui/app/components/panel_switcher.js | 58 ++++ ui/app/components/spinner/index.js | 15 + ui/app/components/spinner/spinner.css | 42 +++ ui/app/components/splash.js | 132 +++++++++ ui/app/components/wizard/add_provider_modal.js | 94 +++++++ ui/app/components/wizard/index.js | 38 +++ ui/app/components/wizard/provider_select_stage.js | 86 ++++++ ui/app/components/wizard/stage_layout.js | 37 +++ ui/app/components/wizard/wizard.less | 44 +++ ui/app/css/bootstrap.less | 5 + ui/app/css/common.css | 80 ++++++ ui/app/img/arrow-down.svg | 65 +++++ ui/app/img/arrow-up.svg | 65 +++++ ui/app/img/cloud.svg | 65 +++++ ui/app/img/disabled.svg | 72 +++++ ui/app/img/envelope.svg | 64 +++++ ui/app/img/gear.svg | 61 ++++ ui/app/img/mask.svg | 133 +++++++++ ui/app/img/off.svg | 88 ++++++ ui/app/img/on.svg | 78 ++++++ ui/app/img/planet.svg | 60 ++++ ui/app/img/unknown.svg | 82 ++++++ ui/app/img/user.svg | 65 +++++ ui/app/img/wait.svg | 75 +++++ ui/app/index.html | 13 + ui/app/lib/bitmask.js | 306 +++++++++++++++++++++ ui/app/lib/color.js | 65 +++++ ui/app/lib/colors.js | 289 +++++++++++++++++++ ui/app/lib/common.js | 7 + ui/app/lib/validate.js | 82 ++++++ ui/app/main.js | 26 ++ ui/app/models/account.js | 143 ++++++++++ ui/app/models/dummy_account.js | 33 +++ ui/package.json | 34 +++ ui/pydist/README.md | 4 + ui/pydist/setup.py | 56 ++++ ui/webpack.config.js | 84 ++++++ www/Makefile | 47 ---- www/README.md | 112 -------- www/app/app.js | 33 --- www/app/components/area.js | 65 ----- www/app/components/center.js | 39 --- www/app/components/debug_panel.js | 40 --- www/app/components/error_panel.js | 21 -- www/app/components/greeter_panel.js | 34 --- www/app/components/list_edit.js | 122 -------- www/app/components/login.js | 302 -------------------- www/app/components/main_panel/account_list.js | 113 -------- www/app/components/main_panel/email_section.js | 48 ---- www/app/components/main_panel/index.js | 80 ------ www/app/components/main_panel/main_panel.less | 212 -------------- www/app/components/main_panel/section_layout.js | 59 ---- www/app/components/main_panel/user_section.js | 71 ----- www/app/components/main_panel/vpn_section.js | 0 www/app/components/panel_switcher.js | 58 ---- www/app/components/spinner/index.js | 15 - www/app/components/spinner/spinner.css | 42 --- www/app/components/splash.js | 132 --------- www/app/components/wizard/add_provider_modal.js | 94 ------- www/app/components/wizard/index.js | 38 --- www/app/components/wizard/provider_select_stage.js | 86 ------ www/app/components/wizard/stage_layout.js | 37 --- www/app/components/wizard/wizard.less | 44 --- www/app/css/bootstrap.less | 5 - www/app/css/common.css | 80 ------ www/app/img/arrow-down.svg | 65 ----- www/app/img/arrow-up.svg | 65 ----- www/app/img/cloud.svg | 65 ----- www/app/img/disabled.svg | 72 ----- www/app/img/envelope.svg | 64 ----- www/app/img/gear.svg | 61 ---- www/app/img/mask.svg | 133 --------- www/app/img/off.svg | 88 ------ www/app/img/on.svg | 78 ------ www/app/img/planet.svg | 60 ---- www/app/img/unknown.svg | 82 ------ www/app/img/user.svg | 65 ----- www/app/img/wait.svg | 75 ----- www/app/index.html | 13 - www/app/lib/bitmask.js | 306 --------------------- www/app/lib/color.js | 65 ----- www/app/lib/colors.js | 289 ------------------- www/app/lib/common.js | 7 - www/app/lib/validate.js | 82 ------ www/app/main.js | 26 -- www/app/models/account.js | 143 ---------- www/app/models/dummy_account.js | 33 --- www/package.json | 34 --- www/pydist/README.md | 4 - www/pydist/setup.py | 56 ---- www/webpack.config.js | 84 ------ 113 files changed, 4154 insertions(+), 4154 deletions(-) create mode 100644 ui/Makefile create mode 100644 ui/README.md create mode 100644 ui/app/app.js create mode 100644 ui/app/components/area.js create mode 100644 ui/app/components/center.js create mode 100644 ui/app/components/debug_panel.js create mode 100644 ui/app/components/error_panel.js create mode 100644 ui/app/components/greeter_panel.js create mode 100644 ui/app/components/list_edit.js create mode 100644 ui/app/components/login.js create mode 100644 ui/app/components/main_panel/account_list.js create mode 100644 ui/app/components/main_panel/email_section.js create mode 100644 ui/app/components/main_panel/index.js create mode 100644 ui/app/components/main_panel/main_panel.less create mode 100644 ui/app/components/main_panel/section_layout.js create mode 100644 ui/app/components/main_panel/user_section.js create mode 100644 ui/app/components/main_panel/vpn_section.js create mode 100644 ui/app/components/panel_switcher.js create mode 100644 ui/app/components/spinner/index.js create mode 100644 ui/app/components/spinner/spinner.css create mode 100644 ui/app/components/splash.js create mode 100644 ui/app/components/wizard/add_provider_modal.js create mode 100644 ui/app/components/wizard/index.js create mode 100644 ui/app/components/wizard/provider_select_stage.js create mode 100644 ui/app/components/wizard/stage_layout.js create mode 100644 ui/app/components/wizard/wizard.less create mode 100644 ui/app/css/bootstrap.less create mode 100644 ui/app/css/common.css create mode 100644 ui/app/img/arrow-down.svg create mode 100644 ui/app/img/arrow-up.svg create mode 100644 ui/app/img/cloud.svg create mode 100644 ui/app/img/disabled.svg create mode 100644 ui/app/img/envelope.svg create mode 100644 ui/app/img/gear.svg create mode 100644 ui/app/img/mask.svg create mode 100644 ui/app/img/off.svg create mode 100644 ui/app/img/on.svg create mode 100644 ui/app/img/planet.svg create mode 100644 ui/app/img/unknown.svg create mode 100644 ui/app/img/user.svg create mode 100644 ui/app/img/wait.svg create mode 100644 ui/app/index.html create mode 100644 ui/app/lib/bitmask.js create mode 100644 ui/app/lib/color.js create mode 100644 ui/app/lib/colors.js create mode 100644 ui/app/lib/common.js create mode 100644 ui/app/lib/validate.js create mode 100644 ui/app/main.js create mode 100644 ui/app/models/account.js create mode 100644 ui/app/models/dummy_account.js create mode 100644 ui/package.json create mode 100644 ui/pydist/README.md create mode 100644 ui/pydist/setup.py create mode 100644 ui/webpack.config.js delete mode 100644 www/Makefile delete mode 100644 www/README.md delete mode 100644 www/app/app.js delete mode 100644 www/app/components/area.js delete mode 100644 www/app/components/center.js delete mode 100644 www/app/components/debug_panel.js delete mode 100644 www/app/components/error_panel.js delete mode 100644 www/app/components/greeter_panel.js delete mode 100644 www/app/components/list_edit.js delete mode 100644 www/app/components/login.js delete mode 100644 www/app/components/main_panel/account_list.js delete mode 100644 www/app/components/main_panel/email_section.js delete mode 100644 www/app/components/main_panel/index.js delete mode 100644 www/app/components/main_panel/main_panel.less delete mode 100644 www/app/components/main_panel/section_layout.js delete mode 100644 www/app/components/main_panel/user_section.js delete mode 100644 www/app/components/main_panel/vpn_section.js delete mode 100644 www/app/components/panel_switcher.js delete mode 100644 www/app/components/spinner/index.js delete mode 100644 www/app/components/spinner/spinner.css delete mode 100644 www/app/components/splash.js delete mode 100644 www/app/components/wizard/add_provider_modal.js delete mode 100644 www/app/components/wizard/index.js delete mode 100644 www/app/components/wizard/provider_select_stage.js delete mode 100644 www/app/components/wizard/stage_layout.js delete mode 100644 www/app/components/wizard/wizard.less delete mode 100644 www/app/css/bootstrap.less delete mode 100644 www/app/css/common.css delete mode 100644 www/app/img/arrow-down.svg delete mode 100644 www/app/img/arrow-up.svg delete mode 100644 www/app/img/cloud.svg delete mode 100644 www/app/img/disabled.svg delete mode 100644 www/app/img/envelope.svg delete mode 100644 www/app/img/gear.svg delete mode 100644 www/app/img/mask.svg delete mode 100644 www/app/img/off.svg delete mode 100644 www/app/img/on.svg delete mode 100644 www/app/img/planet.svg delete mode 100644 www/app/img/unknown.svg delete mode 100644 www/app/img/user.svg delete mode 100644 www/app/img/wait.svg delete mode 100644 www/app/index.html delete mode 100644 www/app/lib/bitmask.js delete mode 100644 www/app/lib/color.js delete mode 100644 www/app/lib/colors.js delete mode 100644 www/app/lib/common.js delete mode 100644 www/app/lib/validate.js delete mode 100644 www/app/main.js delete mode 100644 www/app/models/account.js delete mode 100644 www/app/models/dummy_account.js delete mode 100644 www/package.json delete mode 100644 www/pydist/README.md delete mode 100644 www/pydist/setup.py delete mode 100644 www/webpack.config.js diff --git a/.gitignore b/.gitignore index 31d862b4..0fa20c72 100644 --- a/.gitignore +++ b/.gitignore @@ -35,9 +35,9 @@ venv/ ENV/ # Javascript, web-ui -www/node_modules -www/npm-debug.log -www/pydist/bitmask_js +ui/node_modules +ui/npm-debug.log +ui/pydist/bitmask_js # vim *.swp diff --git a/Makefile b/Makefile index 46a6ca80..49472bb5 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ clean: dev-mail: pip install -e '.[mail]' - make -C www dev-install-prebuilt + make -C ui dev-install-prebuilt dev-all: pip install -e '.[all]' diff --git a/README.rst b/README.rst index 6f2f98d6..d8f6a3b9 100644 --- a/README.rst +++ b/README.rst @@ -111,11 +111,11 @@ First, install the javascript prerequisites: Next, run ``dev-install``: source venv/bin/activate # if not already activated - cd www + cd ui make dev-install # install JS user interface as a python package in "develop" mode. node run watch # continually rebuild javascript bundle when source files change. -For more information, see ``www/README.md``. +For more information, see ``ui/README.md``. cross-testing +++++++++++++++++++++++++++++++++++++++ diff --git a/src/leap/bitmask/core/web/README b/src/leap/bitmask/core/web/README index 76a745df..5826c527 100644 --- a/src/leap/bitmask/core/web/README +++ b/src/leap/bitmask/core/web/README @@ -1,9 +1,9 @@ This is the original implementation of the bitmask.js library, which uses the REST api exposed by the HTTPRequestDispatcher. -The development of bitmask_js is in the www/ folder in this bitmask-dev repo. +The development of bitmask_js is in the ui/ folder in this bitmask-dev repo. -A pre-compiled version of the html+js web-ui can be found in the leap.bitmask_www package. +A pre-compiled version of the html+js web-ui can be found in the bitmask_js package. This remains here to be able to develop against the REST api without the need -of installing the full-fledged bitmask_www package. +of installing the full-fledged bitmask_js package. diff --git a/src/leap/bitmask/gui/README.rst b/src/leap/bitmask/gui/README.rst index 431bcfc8..c3a6a0f2 100644 --- a/src/leap/bitmask/gui/README.rst +++ b/src/leap/bitmask/gui/README.rst @@ -1,5 +1,5 @@ bitmask.gui is a module that depends on PyQt. It is declared as an extra in bitmask setup.py -Its function is to launch a minimalistic browser that serves the bitmask_www +Its function is to launch a minimalistic browser that serves the bitmask_js interface. diff --git a/ui/Makefile b/ui/Makefile new file mode 100644 index 00000000..d67a02b3 --- /dev/null +++ b/ui/Makefile @@ -0,0 +1,47 @@ +# +# builds for development mode +# + +dev-build: build-clean + npm install + npm run build + touch pydist/bitmask_js/__init__.py + +dev-install: dev-build + pip install -e pydist + +# +# installs python package, but does not rebuild the js. +# for usage when you don't want to install nodejs +# +dev-install-prebuilt: + pip install -e pydist + + +# +# distribution builds +# + +dist-build: build-clean + npm install + npm run build:production + touch pydist/bitmask_js/__init__.py + cd pydist && python setup.py bdist_wheel + +dist-install: dist-build + pip install pydist/dist/*.whl + +# +# cleaning up +# + +build-clean: + rm -rf pydist/bitmask_js + rm -rf pydist/dist + rm -rf pydist/build + +clean: build-clean + rm -rf node_modules + +uninstall: + pip uninstall bitmask_js diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 00000000..3f276c00 --- /dev/null +++ b/ui/README.md @@ -0,0 +1,112 @@ +Bitmask Javascript UI +================================================================= + +Here lies the user interface for Bitmask, written in Javascript. + +quick start: + + sudo apt install nodejs npm nodejs-legacy + npm install # installs development dependencies in "node_modules" + npm run watch # continually rebuilds source .js into "pydist" + +build for deployment: + + npm install + npm run build:production + +After 'build', 'build:production', or 'watch' is run, everything needed for +Bitmask JS is contained in the 'pydist' directory. No additional files are +needed. Open the pydist/bitmask_js/public/index.html file in a browser or web +widget. + +However! Because of the single origin policy of browsers, you will need to +open public/index.html through the webserver included with bitmaskd (e.g. +http://localhost:7070) + +In order for this JS app to be loaded by bitmask, it must be packaged as a +python package and installed in the virtualenv: + + source path-to-virtualenv/bin/activate + make dev-install # builds and installs JS app as python package + pkill bitmaskd # make sure bitmaskd is not already running + bitmaskd # launch backend + npm run open # opens http://localhost:7070/ in a browser + npm run watch # rebuild JS whenever source file is changed. + +In order to package for distribution: + + make dist-build + +NOTE: If you make changes to the asset files, like add or modify an image, you + will need to stop then rerun `npm run watch` for the changes to take + effect. + +Development Dependencies +----------------------------------------------------------------- + +This application has no "runtime" dependencies: all the javascript needed is +bundled and included. However, there are many development dependencies. +Run `npm ls` for a full list. + +**npm** + +Package management, controlled by the package.json file. + +**webpack** + +Asset bundling and transformation. It takes all your javascript and CSS and +bundles it into one (or more) files, handling support for 'require' and scope +separation. + +loaders & plugins: + +* babel-loader: use Babel with Webpack. + +* style-loader/css-loader: standard css loader. + +* less-loader: allows use the less stylesheet. + +* copy-webpack-plugin: allows us to specify what files to copy using Webpack. + +* extract-text-webpack-plugin: allows you to split js and css into separate + files. + +**babel** + +Babel is used to compile javascript into javascript, transforming along the +way. We have enabled these plugins: + +* babel-presets-react: Adds support for React features, such as JSX (html + inlined in js files). + +* babel-presets-es2015: Allows the use of modern ES6 javascript. + +* babel-presets-stage-0: Allows the use of some ES7 proposals, even though + these are not standardized yet. Makes classes nicer. + +* babel-polyfill: This is not part of the babel transpiling, but is distributed by babel. This polyfill will give you a full ES2015 environment even if the browser is missing some javascript features. We include this in the 'entry' option of the webpack config. https://babeljs.io/docs/usage/polyfill/ + +**react** + +React is an efficient way to generate HTML views with Javascript. It allows you +to create interactive UIs without ever modifying the DOM by using "one way" +data binding. This greatly simplifies the code and reduces errors. + +**bootstrap** + +The world's most popular CSS styles for UI elements. The npm package includes +both pre-compiled css and less stylesheets. Even though Semantic UI is better, +Bootstrap components for React are much more stable, I have found, and also are +easy to theme. + +To integrate Bootstrap with React: + +* react-bootstrap: React components that use Bootstrap styles. These component + include all the needed javascript and don't require JQuery, although they do + require that the bootstrap CSS is loaded independently. + +**zxcvbn** + +A password strength checker that doesn't suck, but which is big. This JS is +only loaded when we think we are about to need it. + diff --git a/ui/app/app.js b/ui/app/app.js new file mode 100644 index 00000000..57120a4e --- /dev/null +++ b/ui/app/app.js @@ -0,0 +1,33 @@ +import bitmask from 'lib/bitmask' +import Account from 'models/account' + +class Application { + constructor() { + } + + // + // main entry point for the application + // + start() { + Account.active().then(account => { + if (account == null) { + this.show('greeter', {onLogin: this.onLogin.bind(this)}) + } else { + this.show('main', {initialAccount: account}) + } + }, error => { + this.show('error', {error: error}) + }) + } + + onLogin(account) { + this.show('main', {initialAccount: account}) + } + + show(panel, properties) { + this.switcher.show(panel, properties) + } +} + +var App = new Application +export default App \ No newline at end of file diff --git a/ui/app/components/area.js b/ui/app/components/area.js new file mode 100644 index 00000000..e903e5f5 --- /dev/null +++ b/ui/app/components/area.js @@ -0,0 +1,65 @@ +// +// A bootstrap panel, but with some extra options +// + +import React from 'react' +// import {Panel} from 'react-bootstrap' + +class Area extends React.Component { + + static get defaultProps() {return{ + position: null, // top or bottom + size: 'small', // small or big + type: null, // light or dark + className: null + }} + + constructor(props) { + super(props) + } + + render() { + let style = {} + let innerstyle = {} + if (this.props.position == 'top') { + style.borderBottomRightRadius = '0px' + style.borderBottomLeftRadius = '0px' + style.marginBottom = '0px' + style.borderBottom = '0px' + if (this.props.size == 'big') { + innerstyle.padding = '25px' + } + } else if (this.props.position == 'bottom') { + style.borderTopRightRadius = '0px' + style.borderTopLeftRadius = '0px' + style.borderTop = '0px' + if (this.props.size == 'big') { + innerstyle.padding = '15px 25px' + } + } + + let type = this.props.type ? "area-" + this.props.type : "" + let className = ['panel', 'panel-default', type, this.props.className].join(' ') + return( +
+
+ {this.props.children} +
+
+ ) + } + +} + +// Area.propTypes = { +// children: React.PropTypes.oneOfType([ +// React.PropTypes.element, +// React.PropTypes.arrayOf(React.PropTypes.element) +// ]) +// } + +//Area.propTypes = { +// children: React.PropTypes.element.isRequired +//} + +export default Area diff --git a/ui/app/components/center.js b/ui/app/components/center.js new file mode 100644 index 00000000..6fa62128 --- /dev/null +++ b/ui/app/components/center.js @@ -0,0 +1,39 @@ +// +// puts a block right in the center of the window +// + +import React from 'react' + +class Center extends React.Component { + + static get defaultProps() {return{ + width: null + }} + + constructor(props) { + super(props) + } + + render() { + let style = null + if (this.props.width) { + style = {width: this.props.width + 'px'} + } + return ( +
+
+ {this.props.children} +
+
+ ) + } +} + +Center.propTypes = { + children: React.PropTypes.oneOfType([ + React.PropTypes.element, + React.PropTypes.arrayOf(React.PropTypes.element) + ]) +} + +export default Center diff --git a/ui/app/components/debug_panel.js b/ui/app/components/debug_panel.js new file mode 100644 index 00000000..7515ba84 --- /dev/null +++ b/ui/app/components/debug_panel.js @@ -0,0 +1,40 @@ +import React from 'react' +import App from '../app' + + +class DebugPanel extends React.Component { + + constructor(props) { + super(props) + this.click = this.click.bind(this) + } + + componentDidMount() { + this.click(window.location.hash.replace('#', '')) + } + + click(panel_name) { + window.location.hash = panel_name + App.show(panel_name) + } + + panel(panel_name) { + return elem( + 'a', + { onClick: () => this.click(panel_name), key: panel_name }, + panel_name + ) + } + + render() { + return elem('div', {className: 'debug-panel'}, + this.panel('splash'), + this.panel('greeter'), + this.panel('wizard'), + this.panel('main') + ) + } + +} + +export default DebugPanel \ No newline at end of file diff --git a/ui/app/components/error_panel.js b/ui/app/components/error_panel.js new file mode 100644 index 00000000..fc88d459 --- /dev/null +++ b/ui/app/components/error_panel.js @@ -0,0 +1,21 @@ +import React from 'react' +import Center from './center' +import Area from './area' + +export default class ErrorPanel extends React.Component { + + constructor(props) { + super(props) + } + + render () { + return ( +
+ +

Error

+ {this.props.error} + +
+ ) + } +} diff --git a/ui/app/components/greeter_panel.js b/ui/app/components/greeter_panel.js new file mode 100644 index 00000000..4552db18 --- /dev/null +++ b/ui/app/components/greeter_panel.js @@ -0,0 +1,34 @@ +import React from 'react' +import Login from './login' +import Center from './center' +import Splash from './splash' +import Area from './area' +import { Glyphicon } from 'react-bootstrap' +import App from 'app' + +export default class GreeterPanel extends React.Component { + + constructor(props) { + super(props) + } + + newAccount() { + App.show('wizard') + } + + render () { + return
+ +
+ + + + + +   + Create a new account... + +
+
+ } +} diff --git a/ui/app/components/list_edit.js b/ui/app/components/list_edit.js new file mode 100644 index 00000000..0d557d22 --- /dev/null +++ b/ui/app/components/list_edit.js @@ -0,0 +1,122 @@ +// +// A simple list of items, with minus and plus buttons to add and remove +// items. +// + +import React from 'react' +import {Button, ButtonGroup, ButtonToolbar, Glyphicon, FormControl} from 'react-bootstrap' + +const CONTAINER_CSS = { + display: "flex", + flexDirection: "column" +} +const SELECT_CSS = { + padding: "0px", + flex: "1 1 1000px", + overflowY: "scroll" +} +const OPTION_CSS = { + padding: "10px" +} +const TOOLBAR_CSS = { + paddingTop: "10px", + flex: "0 0 auto" +} + +class ListEdit extends React.Component { + + static get defaultProps() {return{ + width: null, + items: [ + 'aaaaaaa', + 'bbbbbbb', + 'ccccccc' + ], + selected: null, + onRemove: null, + onAdd: null, + }} + + constructor(props) { + super(props) + let index = 0 + if (props.selected) { + index = props.items.indexOf(props.selected) + } + this.state = { + selected: index + } + this.click = this.click.bind(this) + this.add = this.add.bind(this) + this.remove = this.remove.bind(this) + } + + setSelected(index) { + this.setState({ + selected: index + }) + } + + click(e) { + let row = parseInt(e.target.value) + if (row >= 0) { + this.setState({selected: row}) + } + } + + add() { + if (this.props.onAdd) { + this.props.onAdd() + } + } + + remove() { + if (this.state.selected >= 0 && this.props.onRemove) { + if (this.props.items.length == this.state.selected + 1) { + // if we remove the last item, set the selected item + // to the one right before it. + this.setState({selected: (this.state.selected - 1)}) + } + this.props.onRemove(this.props.items[this.state.selected]) + } + } + + render() { + let options = null + if (this.props.items) { + options = this.props.items.map((item, i) => { + return + }, this) + } + return( +
+ + {options} + + + + + + + +
+ ) + } + +} + +ListEdit.propTypes = { + children: React.PropTypes.oneOfType([ + React.PropTypes.element, + React.PropTypes.arrayOf(React.PropTypes.element) + ]) +} + +export default ListEdit diff --git a/ui/app/components/login.js b/ui/app/components/login.js new file mode 100644 index 00000000..fe4ef5b2 --- /dev/null +++ b/ui/app/components/login.js @@ -0,0 +1,302 @@ +import React from 'react' +import ReactDOM from 'react-dom' + +import { FormGroup, ControlLabel, FormControl, HelpBlock, Button, + Checkbox, Glyphicon, Overlay, Tooltip, Alert } from 'react-bootstrap' +import Spinner from './spinner' + +import Validate from 'lib/validate' +import App from 'app' +import Account from 'models/account' + +class Login extends React.Component { + + static get defaultProps() {return{ + rememberAllowed: false, // if set, show remember password checkbox + domain: null, // if set, only allow this domain + onLogin: null + }} + + constructor(props) { + super(props) + + // validation states can be null, 'success', 'warning', or 'error' + + this.state = { + loading: false, + + authError: false, // authentication error message + + username: "etest1@riseup.net", + usernameState: null, // username validation state + usernameError: false, // username help message + + password: "whatever", + passwordState: null, // password validation state + passwordError: false, // password help message + + disabled: false, + remember: false // remember is checked? + } + + // prebind: + this.onUsernameChange = this.onUsernameChange.bind(this) + this.onUsernameBlur = this.onUsernameBlur.bind(this) + this.onPassword = this.onPassword.bind(this) + this.onSubmit = this.onSubmit.bind(this) + this.onRemember = this.onRemember.bind(this) + } + + componentDidMount() { + Validate.loadPasswdLib() + } + + render () { + let rememberCheck = "" + let submitButton = "" + let usernameHelp = null + let passwordHelp = null + let message = null + + if (this.props.rememberAllowed) { + let props = { + style: {marginTop: "0px"}, + onChange: this.onRemember + } + + if (this.state.remember) { + rememberCheck = + Remember username and password + + } else { + rememberCheck = + Remember username and password + + } + } + + if (this.state.authError) { + // style may be: success, warning, danger, info + message = ( + {this.state.authError} + ) + } + + if (this.state.usernameError) { + usernameHelp = {this.state.usernameError} + // let props = {shouldUpdatePosition: true, show:true, placement:"right", + // target:this.refs.username} + // usernameHelp = ( + // + // {this.state.usernameError} + // + // ) + } else { + //usernameHelp =   + } + + if (this.state.passwordError) { + passwordHelp = {this.state.passwordError} + // let props = {shouldUpdatePosition: true, show:true, placement:"right", + // target:this.refs.password, component: {this}} + // passwordHelp = ( + // + // {this.state.passwordError} + // + // ) + } else { + //passwordHelp =   + } + + let buttonProps = { + type: "button", + onClick: this.onSubmit, + disabled: !this.maySubmit() + } + if (this.state.loading) { + submitButton = + } else { + submitButton = + } + + let usernameref = null + if (this.props.domain) { + usernameref = function(c) { + if (c != null) { + let textarea = ReactDOM.findDOMNode(c) + let start = textarea.value.indexOf('@') + if (textarea.selectionStart > start) { + textarea.setSelectionRange(start, start) + } + } + } + } + + let form =
+ {message} + + Username + + {this.state.usernameState == 'success' ? null : } + {usernameHelp} + + + + Password + + {this.state.passwordState == 'success' ? null : } + {passwordHelp} + + + {submitButton} + {rememberCheck} +
+ + return form + } + + // + // Here we do a partial validation, because the user has not stopped typing. + // + onUsernameChange(e) { + let username = e.target.value.toLowerCase().replace("\n", "") + if (this.props.domain) { + let [userpart, domainpart] = username.split( + new RegExp('@|' + this.props.domain.replace(".", "\\.") + '$') + ) + username = [userpart, this.props.domain].join('@') + } + let error = Validate.usernameInteractive(username, this.props.domain) + let state = null + if (error) { + state = 'error' + } else { + if (username && username.length > 0) { + let finalError = Validate.username(username) + state = finalError ? null : 'success' + } + } + this.setState({ + username: username, + usernameState: state, + usernameError: error ? error : null + }) + } + + // + // Here we do a more complete validation, since the user have left the field. + // + onUsernameBlur(e) { + let username = e.target.value.toLowerCase() + this.setState({ + username: username + }) + if (username.length > 0) { + this.validateUsername(username) + } else { + this.setState({ + usernameState: null, + usernameError: null + }) + } + } + + onPassword(e) { + let password = e.target.value + this.setState({password: password}) + if (password.length > 0) { + this.validatePassword(password) + } else { + this.setState({ + passwordState: null, + passwordError: null + }) + } + } + + onRemember(e) { + let currentValue = e.target.value == 'on' ? true : false + let value = !currentValue + this.setState({remember: value}) + } + + validateUsername(username) { + let error = Validate.username(username, this.props.domain) + this.setState({ + usernameState: error ? 'error' : 'success', + usernameError: error ? error : null + }) + } + + validatePassword(password) { + let state = null + let message = null + let result = Validate.passwordStrength(password) + if (result) { + message = "Time to crack: " + result.crack_times_display.offline_slow_hashing_1e4_per_second + if (result.score == 0) { + state = 'error' + } else if (result.score == 1 || result.score == 2) { + state = 'warning' + } else { + state = 'success' + } + } + this.setState({ + passwordState: state, + passwordError: message + }) + } + + maySubmit() { + return( + !this.stateLoading && + !this.state.usernameError && + this.state.username != "" && + this.state.password != "" + ) + } + + onSubmit(e) { + e.preventDefault() // don't reload the page please! + if (!this.maySubmit()) { return } + this.setState({loading: true}) + + let account = Account.find(this.state.username) + account.login(this.state.password).then( + account => { + this.setState({loading: false}) + if (this.props.onLogin) { + this.props.onLogin(account) + } + }, + error => { + console.log(error) + if (error == "") { + error = 'Something failed, but we did not get a message' + } + this.setState({ + loading: false, + usernameState: 'error', + passwordState: 'error', + authError: error + }) + } + ) + } + +} + +export default Login \ No newline at end of file diff --git a/ui/app/components/main_panel/account_list.js b/ui/app/components/main_panel/account_list.js new file mode 100644 index 00000000..d0ef092f --- /dev/null +++ b/ui/app/components/main_panel/account_list.js @@ -0,0 +1,113 @@ +import React from 'react' +import {Button, ButtonGroup, ButtonToolbar, Glyphicon} from 'react-bootstrap' + +import App from 'app' +import Account from 'models/account' + +export default class AccountList extends React.Component { + + static get defaultProps() {return{ + account: null, + accounts: [], + onAdd: null, + onRemove: null, + onSelect: null + }} + + constructor(props) { + super(props) + + this.state = { + mode: 'expanded' + } + + // prebind: + this.select = this.select.bind(this) + this.add = this.add.bind(this) + this.remove = this.remove.bind(this) + this.expand = this.expand.bind(this) + this.collapse = this.collapse.bind(this) + } + + select(e) { + let account = this.props.accounts.find( + account => account.id == e.currentTarget.dataset.id + ) + if (this.props.onSelect) { + this.props.onSelect(account) + } + } + + add() { + App.show('wizard') + } + + remove() { + } + + expand() { + this.setState({mode: 'expanded'}) + } + + collapse() { + this.setState({mode: 'collapsed'}) + } + + render() { + let style = {} + let expandButton = null + let plusminusButtons = null + + if (this.state.mode == 'expanded') { + expandButton = ( + + ) + plusminusButtons = ( + + + + + ) + } else { + style.width = '60px' + expandButton = ( + + ) + } + + let items = this.props.accounts.map((account, i) => { + let className = account == this.props.account ? 'active' : 'inactive' + return ( +
  • + {account.userpart} + {account.domain} + + +
  • + ) + }) + + + return ( +
    + + + {plusminusButtons} + {expandButton} + +
    + ) + } + + +} diff --git a/ui/app/components/main_panel/email_section.js b/ui/app/components/main_panel/email_section.js new file mode 100644 index 00000000..a6525d92 --- /dev/null +++ b/ui/app/components/main_panel/email_section.js @@ -0,0 +1,48 @@ +import React from 'react' +//import { Button, Glyphicon, Alert } from 'react-bootstrap' +import SectionLayout from './section_layout' +import Account from 'models/account' +import Spinner from 'components/spinner' +import bitmask from 'lib/bitmask' + +export default class EmailSection extends React.Component { + + static get defaultProps() {return{ + account: null + }} + + constructor(props) { + super(props) + this.state = { + status: null + } + this.openKeys = this.openKeys.bind(this) + this.openApp = this.openApp.bind(this) + this.openPrefs = this.openPrefs.bind(this) + + console.log('email constructor') + } + + openKeys() {} + openApp() {} + openPrefs() {} + + render () { + //let message = null + //if (this.state.error) { + // // style may be: success, warning, danger, info + // message = ( + // {this.state.error} + // ) + //} + let button = null + if (this.state.status == 'ready') { + button = + } + return ( + +

    inbox:

    +
    + ) + } +} diff --git a/ui/app/components/main_panel/index.js b/ui/app/components/main_panel/index.js new file mode 100644 index 00000000..3cc6c11f --- /dev/null +++ b/ui/app/components/main_panel/index.js @@ -0,0 +1,80 @@ +// +// The main panel manages the current account and the list of available accounts +// +// It displays multiple sections, one for each service. +// + +import React from 'react' +import App from 'app' +import Login from 'components/login' +import Account from 'models/account' +import DummyAccount from 'models/dummy_account' + +import './main_panel.less' +import AccountList from './account_list' +import UserSection from './user_section' +import EmailSection from './email_section' + +export default class MainPanel extends React.Component { + + static get defaultProps() {return{ + initialAccount: null + }} + + constructor(props) { + super(props) + this.state = { + account: null, + accounts: [] + } + this.activateAccount = this.activateAccount.bind(this) + } + + componentWillMount() { + if (this.props.initialAccount) { + console.log(Account.list) + Account.add(this.props.initialAccount) + Account.add(new DummyAccount(this.props.initialAccount)) + this.setState({ + account: this.props.initialAccount, + accounts: Account.list + }) + } + } + + activateAccount(account) { + this.setState({ + account: account, + accounts: Account.list + }) + } + + //setAccounts(accounts) { + // this.setState({ + // accounts: accounts + // }) + //} + + render() { + let emailSection = null + let vpnSection = null + + if (this.state.account.authenticated) { + if (this.state.account.hasEmail) { + emailSection = + } + } + + return ( +
    + +
    + + {vpnSection} + {emailSection} +
    +
    + ) + } + +} diff --git a/ui/app/components/main_panel/main_panel.less b/ui/app/components/main_panel/main_panel.less new file mode 100644 index 00000000..4e0ecb05 --- /dev/null +++ b/ui/app/components/main_panel/main_panel.less @@ -0,0 +1,212 @@ +// The space around account entries: +@accounts-padding: 8px; +@accounts-corner: 6px; +@accounts-width: 200px; + +// +// LAYOUT +// + +.main-panel { + position: absolute; + height: 100%; + width: 100%; + display: -webkit-flex; + -webkit-flex-direction: row; + + > .body { + -webkit-flex: 1 1 auto; + overflow: auto; + } + + .accounts { + -webkit-flex: 0 0 auto; + overflow-y: auto; + overflow-x: hidden; + + display: -webkit-flex; + -webkit-flex-direction: column; + ul { + -webkit-flex: 1 1 1000px; + } + .btn-toolbar { + -webkit-flex: 0 0 auto; + } + + } + +} + +// +// Style +// + + +.main-panel > .body { + padding: 20px; +} + +.main-panel .accounts { + background-color: #333; + width: @accounts-width; + padding: @accounts-padding; + padding-right: 0px; +} + +.main-panel .accounts ul { + list-style: none; + margin: 0; + padding: 0; +} + +.main-panel .accounts li { + position: relative; + cursor: pointer; + color: white; + padding: 15px; + background-color: #444; + margin-bottom: @accounts-padding; + border-top-left-radius: @accounts-corner - 1; + border-bottom-left-radius: @accounts-corner - 1; + z-index: 100; +} + +.main-panel .accounts li span.domain { + display: block; + font-weight: bold; + //margin-left: 40px; +} + +.main-panel .accounts li span.username { + display: block; + //margin-left: 40px; + //line-height: 7px; + //margin-bottom: 4px; +} + +/*.main-panel .accounts li span.icon { + display: block; + height: 32px; + width: 32px; + background-color: #999; + float: left; +} +*/ + +.main-panel .accounts li.active { + background-color: white; + color: #333; +} + +.main-panel .accounts li.active span.arc { + display: block; + height: @accounts-corner; + width: @accounts-corner; + background-color: white; + position: absolute; + right: 0; +} + +.main-panel .accounts li.active span.arc.top { + top: 0; + margin-top: -@accounts-corner; +} +.main-panel .accounts li.active span.arc.bottom { + bottom: 0; + margin-bottom: -@accounts-corner; +} +.main-panel .accounts li.active span.arc:after { + display: block; + content: ""; + border-radius: 100%; + height: 0px; + width: 0px; + margin-left: -@accounts-corner; +} +.main-panel .accounts li.active span.arc.top:after { + border: @accounts-corner solid transparent; + border-right: @accounts-corner solid #333; + margin-top: -@accounts-corner; + -webkit-transform: rotate(45deg); + transform: rotate(45deg); +} +.main-panel .accounts li.active span.arc.bottom:after { + border: @accounts-corner solid #333; +} + +.main-panel .accounts .btn.expander { + margin-right: @accounts-padding; +} + +// +// SECTIONS +// + +@icon-size: 32px; +@status-size: 24px; +@section-padding: 10px; + +// service sections layout + +.main-panel .service-section { + display: -webkit-flex; + -webkit-flex-direction: row; + > .icon { + -webkit-flex: 0 0 auto; + } + > .body { + -webkit-flex: 1 1 auto; + } + > .buttons { + -webkit-flex: 0 0 auto; + } + > .status { + -webkit-flex: 0 0 auto; + display: -webkit-flex; + -webkit-align-items: center; + } + +} + +.main-panel .service-section div { + //outline: 1px solid rgba(0,0,0,0.1); +} + +// service sections style + +.main-panel .service-section { + background: #f6f6f6; + border-radius: 4px; + padding: 10px; + margin-bottom: 10px; + &.wide-margin { + padding: 20px 20px 20px 10px; // arbitrary, looks nice + } + > .icon { + padding-right: @section-padding; + img { + width: @icon-size; + height: @icon-size; + } + } + > .body { + h1 { + margin: 0; + padding: 0; + font-size: @icon-size - 10; + line-height: @icon-size; + } + } + > .buttons { + padding-left: 10px; + } + > .status { + padding-left: @section-padding; + width: @section-padding + @status-size; + img { + width: @status-size; + height: @status-size; + } + } +} + diff --git a/ui/app/components/main_panel/section_layout.js b/ui/app/components/main_panel/section_layout.js new file mode 100644 index 00000000..e7c6f2ab --- /dev/null +++ b/ui/app/components/main_panel/section_layout.js @@ -0,0 +1,59 @@ +// +// This is the layout for a service section in the main window. +// It does not do anything except for arrange items using css and html. +// + +import React from 'react' + +export default class SectionLayout extends React.Component { + + static get defaultProps() {return{ + icon: null, + buttons: null, + status: null, + className: "", + style: {} + }} + + constructor(props) { + super(props) + } + + render() { + let className = ["service-section", this.props.className].join(' ') + let status = null + let icon = null + let buttons = null + + if (this.props.status) { + status = ( +
    + +
    + ) + } + if (this.props.icon) { + icon = ( +
    + +
    + ) + } + if (this.props.buttons) + buttons = ( +
    + {this.props.buttons} +
    + ) + return( +
    + {icon} +
    + {this.props.children} +
    + {buttons} + {status} +
    + ) + } +} diff --git a/ui/app/components/main_panel/user_section.js b/ui/app/components/main_panel/user_section.js new file mode 100644 index 00000000..0b4ba136 --- /dev/null +++ b/ui/app/components/main_panel/user_section.js @@ -0,0 +1,71 @@ +import React from 'react' +import { Button, Glyphicon, Alert } from 'react-bootstrap' +import SectionLayout from './section_layout' +import Login from 'components/login' +import Spinner from 'components/spinner' +import Account from 'models/account' + +import bitmask from 'lib/bitmask' + +export default class UserSection extends React.Component { + + static get defaultProps() {return{ + account: null, + onLogout: null, + onLogin: null + }} + + constructor(props) { + super(props) + this.state = { + error: null, + loading: false + } + this.logout = this.logout.bind(this) + } + + logout() { + this.setState({loading: true}) + this.props.account.logout().then( + account => { + this.setState({error: null, loading: false}) + if (this.props.onLogout) { + this.props.onLogout(account) + } + }, error => { + this.setState({error: error, loading: false}) + } + ) + } + + render () { + let message = null + if (this.state.error) { + // style may be: success, warning, danger, info + message = ( + {this.state.error} + ) + } + + if (this.props.account.authenticated) { + let button = null + if (this.state.loading) { + button = + } else { + button = + } + return ( + +

    {this.props.account.address}

    + {message} +
    + ) + } else { + return ( + + + + ) + } + } +} diff --git a/ui/app/components/main_panel/vpn_section.js b/ui/app/components/main_panel/vpn_section.js new file mode 100644 index 00000000..e69de29b diff --git a/ui/app/components/panel_switcher.js b/ui/app/components/panel_switcher.js new file mode 100644 index 00000000..aaf2dc5b --- /dev/null +++ b/ui/app/components/panel_switcher.js @@ -0,0 +1,58 @@ +import React from 'react' +import ReactDOM from 'react-dom' + +import DebugPanel from './debug_panel' +import Splash from './splash' +import GreeterPanel from './greeter_panel' +import MainPanel from './main_panel' +import Wizard from './wizard' + +import App from 'app' +import 'lib/common' + +export default class PanelSwitcher extends React.Component { + + constructor(props) { + super(props) + this.state = { + panel: null, + panel_properties: null, + debug: false + } + App.switcher = this + } + + show(component_name, properties={}) { + this.setState({panel: component_name, panel_properties: properties}) + } + + render() { + let elems = [] + if (this.panelExist(this.state.panel)) { + elems.push( + this.panelRender(this.state.panel, this.state.panel_properties) + ) + } + if (this.state.debug) { + elems.push( + elem(DebugPanel, {key: 'debug'}) + ) + } + return
    {elems}
    + } + + panelExist(panel) { + return panel && this['render_'+panel] + } + + panelRender(panel_name, props) { + let panel = this['render_'+panel_name](props) + return elem('div', {key: 'panel'}, panel) + } + + render_splash(props) {return elem(Splash, props)} + render_wizard(props) {return elem(Wizard, props)} + render_greeter(props) {return elem(GreeterPanel, props)} + render_main(props) {return elem(MainPanel, props)} + +} diff --git a/ui/app/components/spinner/index.js b/ui/app/components/spinner/index.js new file mode 100644 index 00000000..ffc32850 --- /dev/null +++ b/ui/app/components/spinner/index.js @@ -0,0 +1,15 @@ +import React from 'react'; +import './spinner.css'; + +class Spinner extends React.Component { + render() { + let props = {} + return
    +
    +
    +
    +
    + } +} + +export default Spinner \ No newline at end of file diff --git a/ui/app/components/spinner/spinner.css b/ui/app/components/spinner/spinner.css new file mode 100644 index 00000000..5e8535c9 --- /dev/null +++ b/ui/app/components/spinner/spinner.css @@ -0,0 +1,42 @@ +.spinner { + height: 18px; + display: inline-block; +} + +.spinner > div { + width: 18px; + height: 18px; + background-color: #000; + vertical-align: middle; + border-radius: 100%; + display: inline-block; + -webkit-animation: bouncedelay 1.5s infinite ease-in-out; + animation: bouncedelay 1.5s infinite ease-in-out; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} + +.spinner .spin1 { + -webkit-animation-delay: -.46s; + animation-delay: -.46s; +} +.spinner .spin2 { + -webkit-animation-delay: -.24s; + animation-delay: -.24s; +} + +@-webkit-keyframes bouncedelay { + 0%, 80%, 100% { + -webkit-transform: scale(0.5); + } 40% { + -webkit-transform: scale(0.9); + } +} + +@keyframes bouncedelay { + 0%, 80%, 100% { + transform: scale(0.5); + } 40% { + transform: scale(0.9); + } +} diff --git a/ui/app/components/splash.js b/ui/app/components/splash.js new file mode 100644 index 00000000..46170d23 --- /dev/null +++ b/ui/app/components/splash.js @@ -0,0 +1,132 @@ +/* + * A simple animated splash screen + */ + +import React from 'react' +import * as COLOR from '../lib/colors' + +const colorList = [ + COLOR.red200, COLOR.pink200, COLOR.purple200, COLOR.deepPurple200, + COLOR.indigo200, COLOR.blue200, COLOR.lightBlue200, COLOR.cyan200, + COLOR.teal200, COLOR.green200, COLOR.lightGreen200, COLOR.lime200, + COLOR.yellow200, COLOR.amber200, COLOR.orange200, COLOR.deepOrange200 +] + +export default class Splash extends React.Component { + + static get defaultProps() {return{ + speed: "fast", + mask: true, + onClick: null + }} + + constructor(props) { + super(props) + this.counter = 0 + this.interval = null + this.ctx = null + this.stepAngle = 0 + this.resize = this.resize.bind(this) + this.click = this.click.bind(this) + if (this.props.speed == "fast") { + this.fps = 30 + this.stepAngle = 0.005 + } else { + this.fps = 30 + this.stepAngle = 0.0005 + } + } + + componentDidMount() { + this.interval = setInterval(this.tick.bind(this), 1000/this.fps) + this.canvas = this.refs.canvas + this.ctx = this.canvas.getContext('2d') + window.addEventListener('resize', this.resize) + } + + componentWillUnmount() { + clearInterval(this.interval) + window.removeEventListener('resize', this.resize) + } + + click() { + if (this.props.onClick) { + this.props.onClick() + } + } + + tick() { + this.counter++ + this.updateCanvas() + } + + resize() { + this.canvas.width = window.innerWidth + this.canvas.height = window.innerHeight + this.updateCanvas() + } + + updateCanvas() { + const arcCount = 16 + const arcAngle = 1 / arcCount + const x = this.canvas.width / 2 + const y = this.canvas.height / 2 + const radius = screen.height + screen.width + + for (let i = 0; i < arcCount; i++) { + let startAngle = Math.PI * 2 * i/arcCount + this.stepAngle*this.counter + let endAngle = Math.PI * 2 * (i+1)/arcCount + this.stepAngle*this.counter + + this.ctx.fillStyle = colorList[i % colorList.length] + this.ctx.strokeStyle = colorList[i % colorList.length] + this.ctx.beginPath() + this.ctx.moveTo(x, y) + this.ctx.arc(x, y, radius, startAngle, endAngle) + this.ctx.lineTo(x, y) + this.ctx.fill() + this.ctx.stroke() + } + + } + + render () { + let overlay = null + let mask = null + if (this.props.onClick) { + overlay = React.DOM.div({ + style: { + position: 'absolute', + height: '100%', + width: '100%', + backgroundColor: 'transparent' + }, + onClick: this.click + }) + } + if (this.props.mask) { + mask = React.DOM.img({ + src: 'img/mask.svg', + style: { + position: 'absolute', + left: '50%', + top: '50%', + marginLeft: -330/2 + 'px', + marginTop: -174/2 + 'px', + } + }) + } + return React.DOM.div( + {style: {overflow: 'hidden'}}, + React.DOM.canvas({ + ref: 'canvas', + style: {position: 'absolute'}, + width: window.innerWidth, + height: window.innerHeight, + }), + mask, + overlay + ) + } + +} + diff --git a/ui/app/components/wizard/add_provider_modal.js b/ui/app/components/wizard/add_provider_modal.js new file mode 100644 index 00000000..bc5e0236 --- /dev/null +++ b/ui/app/components/wizard/add_provider_modal.js @@ -0,0 +1,94 @@ +// +// A modal popup to add a new provider. +// + +import React from 'react' +import { FormGroup, ControlLabel, FormControl, HelpBlock, Button, Modal } from 'react-bootstrap' +import Spinner from '../spinner' +import Validate from '../../lib/validate' +import App from '../../app' + +class AddProviderModal extends React.Component { + + static get defaultProps() {return{ + title: 'Add a provider', + onClose: null + }} + + constructor(props) { + super(props) + this.state = { + validationState: null, + errorMsg: null, + domain: "" + } + this.accept = this.accept.bind(this) + this.cancel = this.cancel.bind(this) + this.changed = this.changed.bind(this) + } + + accept() { + if (this.state.domain) { + App.providers.add(this.state.domain) + } + this.props.onClose() + } + + cancel() { + this.props.onClose() + } + + changed(e) { + let domain = e.target.value + let newState = null + let newMsg = null + + if (domain.length > 0) { + let error = Validate.domain(domain) + newState = error ? 'error' : 'success' + newMsg = error + } + this.setState({ + domain: domain, + validationState: newState, + errorMsg: newMsg + }) + } + + render() { + let help = null + if (this.state.errorMsg) { + help = {this.state.errorMsg} + } else { + help =   + } + let form =
    + + Domain + + + {help} + + +
    + + return( + + + {this.props.title} + + + {form} + + + ) + } +} + +export default AddProviderModal \ No newline at end of file diff --git a/ui/app/components/wizard/index.js b/ui/app/components/wizard/index.js new file mode 100644 index 00000000..613b88fd --- /dev/null +++ b/ui/app/components/wizard/index.js @@ -0,0 +1,38 @@ +// +// The provider setup wizard +// + +import React from 'react' +import App from 'app' + +import ProviderSelectStage from './provider_select_stage' +import './wizard.less' + +export default class Wizard extends React.Component { + + constructor(props) { + super(props) + this.state = { + stage: 'provider' + } + } + + setStage(stage) { + this.setState({stage: stage}) + } + + render() { + let stage = null + switch(this.state.stage) { + case 'provider': + stage = + break + } + return( +
    + {stage} +
    + ) + } + +} diff --git a/ui/app/components/wizard/provider_select_stage.js b/ui/app/components/wizard/provider_select_stage.js new file mode 100644 index 00000000..20674be1 --- /dev/null +++ b/ui/app/components/wizard/provider_select_stage.js @@ -0,0 +1,86 @@ +import React from 'react' +import {Button, ButtonGroup, ButtonToolbar, Glyphicon} from 'react-bootstrap' + +import App from 'app' +import ListEdit from 'components/list_edit' +import StageLayout from './stage_layout' +import AddProviderModal from './add_provider_modal' + +export default class ProviderSelectStage extends React.Component { + + static get defaultProps() {return{ + title: "Choose a provider", + subtitle: "This doesn't work yet" + }} + + constructor(props) { + super(props) + let domains = this.currentDomains() + this.state = { + domains: domains, + showModal: false + } + this.add = this.add.bind(this) + this.remove = this.remove.bind(this) + this.close = this.close.bind(this) + this.previous = this.previous.bind(this) + } + + currentDomains() { + // return(App.providers.domains().slice() || []) + return ['domain1', 'domain2', 'domain3'] + } + + add() { + this.setState({showModal: true}) + } + + remove(provider) { + // App.providers.remove(provider) + this.setState({domains: this.currentDomains()}) + } + + close() { + let domains = this.currentDomains() + if (domains.length != this.state.domains.length) { + // this is ugly, but i could not get selection working + // by passing it as a property + this.refs.list.setSelected(0) + } + this.setState({ + domains: domains, + showModal: false + }) + } + + previous() { + App.start() + } + + render() { + let modal = null + if (this.state.showModal) { + modal = + } + let buttons = ( + + + + + ) + let select = + return( + + {select} + {modal} + + ) + } +} diff --git a/ui/app/components/wizard/stage_layout.js b/ui/app/components/wizard/stage_layout.js new file mode 100644 index 00000000..31540221 --- /dev/null +++ b/ui/app/components/wizard/stage_layout.js @@ -0,0 +1,37 @@ +import React from 'react' + +class StageLayout extends React.Component { + + static get defaultProps() {return{ + title: 'untitled', + subtitle: null, + buttons: null + }} + + constructor(props) { + super(props) + } + + render() { + let subtitle = null + if (this.props.subtitle) { + subtitle = {this.props.subtitle} + } + return( +
    +
    + {this.props.title} + {subtitle} +
    +
    + {this.props.children} +
    +
    + {this.props.buttons} +
    +
    + ) + } +} + +export default StageLayout \ No newline at end of file diff --git a/ui/app/components/wizard/wizard.less b/ui/app/components/wizard/wizard.less new file mode 100644 index 00000000..29efc20e --- /dev/null +++ b/ui/app/components/wizard/wizard.less @@ -0,0 +1,44 @@ +.wizard .stage { + position: absolute; + height: 100%; + width: 100%; + + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; + -webkit-flex: 1; + flex: 1; +} + +.wizard .stage .footer { + -webkit-flex: 0 0 auto; + flex: 0 0 auto; + background-color: #ddd; + padding: 20px; + text-align: right; +} + +.wizard .stage .header { + -webkit-flex: 0 0 auto; + flex: 0 0 auto; + padding: 20px; + background-color: #333; + color: white; + font-size: 2em; + span { + margin-left: 10px; + font-size: 0.5em; + } +} + +.wizard .stage .body { + -webkit-flex: 1 1 auto; + flex: 1 1 auto; + padding: 20px; + overflow: auto; + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; +} \ No newline at end of file diff --git a/ui/app/css/bootstrap.less b/ui/app/css/bootstrap.less new file mode 100644 index 00000000..3b772284 --- /dev/null +++ b/ui/app/css/bootstrap.less @@ -0,0 +1,5 @@ +// +// require npm modules 'bootstrap' +// +@import "~bootstrap/less/bootstrap"; + diff --git a/ui/app/css/common.css b/ui/app/css/common.css new file mode 100644 index 00000000..acf164ee --- /dev/null +++ b/ui/app/css/common.css @@ -0,0 +1,80 @@ +body { + padding: 0; + margin: 0; +} + +.debug-panel { + background-color: rgba(0,0,0,0.1); + position: absolute; + bottom: 0; + left: 50%; + z-index: 1000; + padding: 20px; +} +.debug-panel a { + cursor: pointer; + margin: 10px; +} + + +.area-light { + background-color: #f7f7f7; +} +.area-dark { + background-color: #e7e7e7; +} +.area-clear { + background-color: #fff; +} + +/* + * Greeter + */ +.greeter { + border-color: #555; + border-width: 8px; +} + +/* + * bootstrap + */ + +.help-block { + margin: 2px 0 0 0; + font-size: small; + opacity: 0.7; +} +.btn.btn-inverse { + color: white; + background-color: #333; +} +.btn.btn-flat { + border-color: transparent; + background-color: transparent; +} + +/*.btn.btn-default { + background-color: #eee !important; +} +*/ + +/* + * center component + */ + +.center-container { + position: absolute; + display: -webkit-flex; + -webkit-flex-flow: row nowrap; + -webkit-justify-content: center; + -webkit-align-content: center; + -webkit-align-items: center; + top: 0px; + left: 0px; + height: 100%; + width: 100%; +} + +.center-container .center-item { + -webkit-flex: 0 1 auto; +} diff --git a/ui/app/img/arrow-down.svg b/ui/app/img/arrow-down.svg new file mode 100644 index 00000000..14c4b5d1 --- /dev/null +++ b/ui/app/img/arrow-down.svg @@ -0,0 +1,65 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/ui/app/img/arrow-up.svg b/ui/app/img/arrow-up.svg new file mode 100644 index 00000000..30ea8fde --- /dev/null +++ b/ui/app/img/arrow-up.svg @@ -0,0 +1,65 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/ui/app/img/cloud.svg b/ui/app/img/cloud.svg new file mode 100644 index 00000000..bf7a94af --- /dev/null +++ b/ui/app/img/cloud.svg @@ -0,0 +1,65 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/ui/app/img/disabled.svg b/ui/app/img/disabled.svg new file mode 100644 index 00000000..90804d83 --- /dev/null +++ b/ui/app/img/disabled.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/ui/app/img/envelope.svg b/ui/app/img/envelope.svg new file mode 100644 index 00000000..5775e097 --- /dev/null +++ b/ui/app/img/envelope.svg @@ -0,0 +1,64 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/ui/app/img/gear.svg b/ui/app/img/gear.svg new file mode 100644 index 00000000..be5a5b33 --- /dev/null +++ b/ui/app/img/gear.svg @@ -0,0 +1,61 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/ui/app/img/mask.svg b/ui/app/img/mask.svg new file mode 100644 index 00000000..f254c5e0 --- /dev/null +++ b/ui/app/img/mask.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/ui/app/img/off.svg b/ui/app/img/off.svg new file mode 100644 index 00000000..35c49a56 --- /dev/null +++ b/ui/app/img/off.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/ui/app/img/on.svg b/ui/app/img/on.svg new file mode 100644 index 00000000..36938e0a --- /dev/null +++ b/ui/app/img/on.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/ui/app/img/planet.svg b/ui/app/img/planet.svg new file mode 100644 index 00000000..5697d5d6 --- /dev/null +++ b/ui/app/img/planet.svg @@ -0,0 +1,60 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/ui/app/img/unknown.svg b/ui/app/img/unknown.svg new file mode 100644 index 00000000..60038638 --- /dev/null +++ b/ui/app/img/unknown.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/ui/app/img/user.svg b/ui/app/img/user.svg new file mode 100644 index 00000000..ad86a757 --- /dev/null +++ b/ui/app/img/user.svg @@ -0,0 +1,65 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/ui/app/img/wait.svg b/ui/app/img/wait.svg new file mode 100644 index 00000000..2ae0113a --- /dev/null +++ b/ui/app/img/wait.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/ui/app/index.html b/ui/app/index.html new file mode 100644 index 00000000..440e0b6f --- /dev/null +++ b/ui/app/index.html @@ -0,0 +1,13 @@ + + + + + Bitmask + + + + +
    + + + \ No newline at end of file diff --git a/ui/app/lib/bitmask.js b/ui/app/lib/bitmask.js new file mode 100644 index 00000000..fedd5fcd --- /dev/null +++ b/ui/app/lib/bitmask.js @@ -0,0 +1,306 @@ +// bitmask.js +// Copyright (C) 2016 LEAP +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +/** + * bitmask object + * + * Contains all the bitmask API mapped by sections + * - user. User management like login, creation, ... + * - mail. Email service control. + * - keys. Keyring operations. + * - events. For registering to events. + * + * Every function returns a Promise that will be triggered once the request is + * finished or will fail if there was any error. Errors are always user readable + * strings. + */ + +try { + // Use Promises in non-ES6 compliant engines. + eval('import "babel-polyfill";') +} +catch (err) {} + +var bitmask = function(){ + var event_handlers = {}; + + var api_url = '/API/'; + if (window.location.protocol === "file:") { + api_url = 'http://localhost:7070/API/'; + } + + function call(command) { + var url = api_url + command.slice(0, 2).join('/'); + var data = JSON.stringify(command.slice(2)); + + return new Promise(function(resolve, reject) { + var req = new XMLHttpRequest(); + req.open('POST', url); + + req.onload = function() { + if (req.status == 200) { + parseResponse(req.response, resolve, reject); + } + else { + reject(Error(req.statusText)); + } + }; + + req.onerror = function() { + reject(Error("Network Error")); + }; + + req.send(data); + }); + }; + + function parseResponse(raw_response, resolve, reject) { + var response = JSON.parse(raw_response); + if (response.error === null) { + resolve(response.result); + } else { + reject(response.error); + } + }; + + function event_polling() { + call(['events', 'poll']).then(function(response) { + if (response !== null) { + evnt = response[0]; + content = response[1]; + if (evnt in event_handlers) { + event_handlers[evnt](evnt, content); + } + } + event_polling(); + }, function(error) { + setTimeout(event_polling, 5000); + }); + }; + event_polling(); + + function private_str(priv) { + if (priv) { + return 'private' + } + return 'public' + }; + + return { + bonafide: { + provider: { + create: function(domain) { + return call(['bonafide', 'provider', 'create', domain]); + }, + + read: function(domain) { + return call(['bonafide', 'provider', 'read', domain]); + }, + + delete: function(domain) { + return call(['bonafide', 'provider', 'delete', domain]); + }, + + list: function(seeded) { + if (typeof seeded !== 'boolean') { + seeded = false; + } + return call(['bonafide', 'provider', 'list', seeded]); + } + }, + + /** + * uids are of the form user@provider.net + */ + user: { + /** + * Check wich user is active + * + * @return {Promise} The uid of the active user + */ + active: function() { + return call(['bonafide', 'user', 'active']); + }, + + /** + * Register a new user + * + * @param {string} uid The uid to be created + * @param {string} password The user password + * @param {boolean} autoconf If the provider should be autoconfigured if it's not allready known + * If it's not provided it will default to false + */ + create: function(uid, password, autoconf) { + if (typeof autoconf !== 'boolean') { + autoconf = false; + } + return call(['bonafide', 'user', 'create', uid, password, autoconf]); + }, + + /** + * Login + * + * @param {string} uid The uid to log in + * @param {string} password The user password + * @param {boolean} autoconf If the provider should be autoconfigured if it's not allready known + * If it's not provided it will default to false + */ + auth: function(uid, password, autoconf) { + if (typeof autoconf !== 'boolean') { + autoconf = false; + } + return call(['bonafide', 'user', 'authenticate', uid, password, autoconf]); + }, + + /** + * Logout + * + * @param {string} uid The uid to log out. + * If no uid is provided the active user will be used + */ + logout: function(uid) { + if (typeof uid !== 'string') { + uid = ""; + } + return call(['bonafide', 'user', 'logout', uid]); + } + } + }, + + mail: { + /** + * Check the status of the email service + * + * @return {Promise} User readable status + */ + status: function() { + return call(['mail', 'status']); + }, + + /** + * Get the token of the active user. + * + * This token is used as password to authenticate in the IMAP and SMTP services. + * + * @return {Promise} The token + */ + get_token: function() { + return call(['mail', 'get_token']); + } + }, + + /** + * A KeyObject have the following attributes: + * - address {string} the email address for wich this key is active + * - fingerprint {string} the fingerprint of the key + * - length {number} the size of the key bits + * - private {bool} if the key is private + * - uids {[string]} the uids in the key + * - key_data {string} the key content + * - validation {string} the validation level which this key was found + * - expiry_date {string} date when the key expires + * - refreshed_at {string} date of the last refresh of the key + * - audited_at {string} date of the last audit (unused for now) + * - sign_used {bool} if has being used to checking signatures + * - enc_used {bool} if has being used to encrypt + */ + keys: { + /** + * List all the keys in the keyring + * + * @param {boolean} priv Should list private keys? + * If it's not provided the public ones will be listed. + * + * @return {Promise<[KeyObject]>} List of keys in the keyring + */ + list: function(priv) { + return call(['keys', 'list', private_str(priv)]); + }, + + /** + * Export key + * + * @param {string} address The email address of the key + * @param {boolean} priv Should get the private key? + * If it's not provided the public one will be fetched. + * + * @return {Promise} The key + */ + exprt: function(address, priv) { + return call(['keys', 'export', address, private_str(priv)]); + }, + + /** + * Insert key + * + * @param {string} address The email address of the key + * @param {string} rawkey The key material + * @param {string} validation The validation level of the key + * If it's not provided 'Fingerprint' level will be used. + * + * @return {Promise} The key + */ + insert: function(address, rawkey, validation) { + if (typeof validation !== 'string') { + validation = 'Fingerprint'; + } + return call(['keys', 'insert', address, validation, rawkey]); + }, + + /** + * Delete a key + * + * @param {string} address The email address of the key + * @param {boolean} priv Should get the private key? + * If it's not provided the public one will be deleted. + * + * @return {Promise} The key + */ + del: function(address, priv) { + return call(['keys', 'delete', address, private_str(priv)]); + } + }, + + events: { + /** + * Register func for an event + * + * @param {string} evnt The event to register + * @param {function} func The function that will be called on each event. + * It has to be like: function(event, content) {} + * Where content will be a list of strings. + */ + register: function(evnt, func) { + event_handlers[evnt] = func; + return call(['events', 'register', evnt]) + }, + + /** + * Unregister from an event + * + * @param {string} evnt The event to unregister + */ + unregister: function(evnt) { + delete event_handlers[evnt]; + return call(['events', 'unregister', evnt]) + } + } + }; +}(); + +try { + module.exports = bitmask +} catch(err) {} diff --git a/ui/app/lib/color.js b/ui/app/lib/color.js new file mode 100644 index 00000000..5b1dfee9 --- /dev/null +++ b/ui/app/lib/color.js @@ -0,0 +1,65 @@ +// +// Color.hsv().css +// +// RGB values are 0..255 +// HSV values are 0..1 +// + +function compose(value) + return [ + Math.round(value * 255), + Math.round(value * 255), + Math.round(value * 255) + } +} + +class Color { + + constructor(r, g, b, a) { + this.r = r + this.g = g + this.b = b + this.a = a + } + + // + // alternate hsv factory + // + static hsv(h,s,v) { + let out = null + h = h % 360; + s = Math.max(0, Math.min(1, s)) + v = Math.max(0, Math.min(1, v)) + + if (s == 0) { + let grey = Math.ceil(v*255) + out = [grey, grey, grey] + } + + let b = ((1 - s) * v); + let vb = v - b; + let hm = h % 60; + switch((h/60)|0) { + case 0: + out = compose(v, vb * h / 60 + b, b); break + case 1: + out = compose(vb * (60 - hm) / 60 + b, v, b); break + case 2: + out = compose(b, v, vb * hm / 60 + b); break + case 3: + out = compose(b, vb * (60 - hm) / 60 + b, v); break + case 4: + out = compose(vb * hm / 60 + b, b, v); break + case 5: + out = compose(v, b, vb * (60 - hm) / 60 + b); break + } + + return new Color(...out) + } + + css() { + return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})` + } +} + +export default Color \ No newline at end of file diff --git a/ui/app/lib/colors.js b/ui/app/lib/colors.js new file mode 100644 index 00000000..d86ca0a8 --- /dev/null +++ b/ui/app/lib/colors.js @@ -0,0 +1,289 @@ +// +// a palette from google's material design +// + +export const red50 = '#ffebee'; +export const red100 = '#ffcdd2'; +export const red200 = '#ef9a9a'; +export const red300 = '#e57373'; +export const red400 = '#ef5350'; +export const red500 = '#f44336'; +export const red600 = '#e53935'; +export const red700 = '#d32f2f'; +export const red800 = '#c62828'; +export const red900 = '#b71c1c'; +export const redA100 = '#ff8a80'; +export const redA200 = '#ff5252'; +export const redA400 = '#ff1744'; +export const redA700 = '#d50000'; + +export const pink50 = '#fce4ec'; +export const pink100 = '#f8bbd0'; +export const pink200 = '#f48fb1'; +export const pink300 = '#f06292'; +export const pink400 = '#ec407a'; +export const pink500 = '#e91e63'; +export const pink600 = '#d81b60'; +export const pink700 = '#c2185b'; +export const pink800 = '#ad1457'; +export const pink900 = '#880e4f'; +export const pinkA100 = '#ff80ab'; +export const pinkA200 = '#ff4081'; +export const pinkA400 = '#f50057'; +export const pinkA700 = '#c51162'; + +export const purple50 = '#f3e5f5'; +export const purple100 = '#e1bee7'; +export const purple200 = '#ce93d8'; +export const purple300 = '#ba68c8'; +export const purple400 = '#ab47bc'; +export const purple500 = '#9c27b0'; +export const purple600 = '#8e24aa'; +export const purple700 = '#7b1fa2'; +export const purple800 = '#6a1b9a'; +export const purple900 = '#4a148c'; +export const purpleA100 = '#ea80fc'; +export const purpleA200 = '#e040fb'; +export const purpleA400 = '#d500f9'; +export const purpleA700 = '#aa00ff'; + +export const deepPurple50 = '#ede7f6'; +export const deepPurple100 = '#d1c4e9'; +export const deepPurple200 = '#b39ddb'; +export const deepPurple300 = '#9575cd'; +export const deepPurple400 = '#7e57c2'; +export const deepPurple500 = '#673ab7'; +export const deepPurple600 = '#5e35b1'; +export const deepPurple700 = '#512da8'; +export const deepPurple800 = '#4527a0'; +export const deepPurple900 = '#311b92'; +export const deepPurpleA100 = '#b388ff'; +export const deepPurpleA200 = '#7c4dff'; +export const deepPurpleA400 = '#651fff'; +export const deepPurpleA700 = '#6200ea'; + +export const indigo50 = '#e8eaf6'; +export const indigo100 = '#c5cae9'; +export const indigo200 = '#9fa8da'; +export const indigo300 = '#7986cb'; +export const indigo400 = '#5c6bc0'; +export const indigo500 = '#3f51b5'; +export const indigo600 = '#3949ab'; +export const indigo700 = '#303f9f'; +export const indigo800 = '#283593'; +export const indigo900 = '#1a237e'; +export const indigoA100 = '#8c9eff'; +export const indigoA200 = '#536dfe'; +export const indigoA400 = '#3d5afe'; +export const indigoA700 = '#304ffe'; + +export const blue50 = '#e3f2fd'; +export const blue100 = '#bbdefb'; +export const blue200 = '#90caf9'; +export const blue300 = '#64b5f6'; +export const blue400 = '#42a5f5'; +export const blue500 = '#2196f3'; +export const blue600 = '#1e88e5'; +export const blue700 = '#1976d2'; +export const blue800 = '#1565c0'; +export const blue900 = '#0d47a1'; +export const blueA100 = '#82b1ff'; +export const blueA200 = '#448aff'; +export const blueA400 = '#2979ff'; +export const blueA700 = '#2962ff'; + +export const lightBlue50 = '#e1f5fe'; +export const lightBlue100 = '#b3e5fc'; +export const lightBlue200 = '#81d4fa'; +export const lightBlue300 = '#4fc3f7'; +export const lightBlue400 = '#29b6f6'; +export const lightBlue500 = '#03a9f4'; +export const lightBlue600 = '#039be5'; +export const lightBlue700 = '#0288d1'; +export const lightBlue800 = '#0277bd'; +export const lightBlue900 = '#01579b'; +export const lightBlueA100 = '#80d8ff'; +export const lightBlueA200 = '#40c4ff'; +export const lightBlueA400 = '#00b0ff'; +export const lightBlueA700 = '#0091ea'; + +export const cyan50 = '#e0f7fa'; +export const cyan100 = '#b2ebf2'; +export const cyan200 = '#80deea'; +export const cyan300 = '#4dd0e1'; +export const cyan400 = '#26c6da'; +export const cyan500 = '#00bcd4'; +export const cyan600 = '#00acc1'; +export const cyan700 = '#0097a7'; +export const cyan800 = '#00838f'; +export const cyan900 = '#006064'; +export const cyanA100 = '#84ffff'; +export const cyanA200 = '#18ffff'; +export const cyanA400 = '#00e5ff'; +export const cyanA700 = '#00b8d4'; + +export const teal50 = '#e0f2f1'; +export const teal100 = '#b2dfdb'; +export const teal200 = '#80cbc4'; +export const teal300 = '#4db6ac'; +export const teal400 = '#26a69a'; +export const teal500 = '#009688'; +export const teal600 = '#00897b'; +export const teal700 = '#00796b'; +export const teal800 = '#00695c'; +export const teal900 = '#004d40'; +export const tealA100 = '#a7ffeb'; +export const tealA200 = '#64ffda'; +export const tealA400 = '#1de9b6'; +export const tealA700 = '#00bfa5'; + +export const green50 = '#e8f5e9'; +export const green100 = '#c8e6c9'; +export const green200 = '#a5d6a7'; +export const green300 = '#81c784'; +export const green400 = '#66bb6a'; +export const green500 = '#4caf50'; +export const green600 = '#43a047'; +export const green700 = '#388e3c'; +export const green800 = '#2e7d32'; +export const green900 = '#1b5e20'; +export const greenA100 = '#b9f6ca'; +export const greenA200 = '#69f0ae'; +export const greenA400 = '#00e676'; +export const greenA700 = '#00c853'; + +export const lightGreen50 = '#f1f8e9'; +export const lightGreen100 = '#dcedc8'; +export const lightGreen200 = '#c5e1a5'; +export const lightGreen300 = '#aed581'; +export const lightGreen400 = '#9ccc65'; +export const lightGreen500 = '#8bc34a'; +export const lightGreen600 = '#7cb342'; +export const lightGreen700 = '#689f38'; +export const lightGreen800 = '#558b2f'; +export const lightGreen900 = '#33691e'; +export const lightGreenA100 = '#ccff90'; +export const lightGreenA200 = '#b2ff59'; +export const lightGreenA400 = '#76ff03'; +export const lightGreenA700 = '#64dd17'; + +export const lime50 = '#f9fbe7'; +export const lime100 = '#f0f4c3'; +export const lime200 = '#e6ee9c'; +export const lime300 = '#dce775'; +export const lime400 = '#d4e157'; +export const lime500 = '#cddc39'; +export const lime600 = '#c0ca33'; +export const lime700 = '#afb42b'; +export const lime800 = '#9e9d24'; +export const lime900 = '#827717'; +export const limeA100 = '#f4ff81'; +export const limeA200 = '#eeff41'; +export const limeA400 = '#c6ff00'; +export const limeA700 = '#aeea00'; + +export const yellow50 = '#fffde7'; +export const yellow100 = '#fff9c4'; +export const yellow200 = '#fff59d'; +export const yellow300 = '#fff176'; +export const yellow400 = '#ffee58'; +export const yellow500 = '#ffeb3b'; +export const yellow600 = '#fdd835'; +export const yellow700 = '#fbc02d'; +export const yellow800 = '#f9a825'; +export const yellow900 = '#f57f17'; +export const yellowA100 = '#ffff8d'; +export const yellowA200 = '#ffff00'; +export const yellowA400 = '#ffea00'; +export const yellowA700 = '#ffd600'; + +export const amber50 = '#fff8e1'; +export const amber100 = '#ffecb3'; +export const amber200 = '#ffe082'; +export const amber300 = '#ffd54f'; +export const amber400 = '#ffca28'; +export const amber500 = '#ffc107'; +export const amber600 = '#ffb300'; +export const amber700 = '#ffa000'; +export const amber800 = '#ff8f00'; +export const amber900 = '#ff6f00'; +export const amberA100 = '#ffe57f'; +export const amberA200 = '#ffd740'; +export const amberA400 = '#ffc400'; +export const amberA700 = '#ffab00'; + +export const orange50 = '#fff3e0'; +export const orange100 = '#ffe0b2'; +export const orange200 = '#ffcc80'; +export const orange300 = '#ffb74d'; +export const orange400 = '#ffa726'; +export const orange500 = '#ff9800'; +export const orange600 = '#fb8c00'; +export const orange700 = '#f57c00'; +export const orange800 = '#ef6c00'; +export const orange900 = '#e65100'; +export const orangeA100 = '#ffd180'; +export const orangeA200 = '#ffab40'; +export const orangeA400 = '#ff9100'; +export const orangeA700 = '#ff6d00'; + +export const deepOrange50 = '#fbe9e7'; +export const deepOrange100 = '#ffccbc'; +export const deepOrange200 = '#ffab91'; +export const deepOrange300 = '#ff8a65'; +export const deepOrange400 = '#ff7043'; +export const deepOrange500 = '#ff5722'; +export const deepOrange600 = '#f4511e'; +export const deepOrange700 = '#e64a19'; +export const deepOrange800 = '#d84315'; +export const deepOrange900 = '#bf360c'; +export const deepOrangeA100 = '#ff9e80'; +export const deepOrangeA200 = '#ff6e40'; +export const deepOrangeA400 = '#ff3d00'; +export const deepOrangeA700 = '#dd2c00'; + +export const brown50 = '#efebe9'; +export const brown100 = '#d7ccc8'; +export const brown200 = '#bcaaa4'; +export const brown300 = '#a1887f'; +export const brown400 = '#8d6e63'; +export const brown500 = '#795548'; +export const brown600 = '#6d4c41'; +export const brown700 = '#5d4037'; +export const brown800 = '#4e342e'; +export const brown900 = '#3e2723'; + +export const blueGrey50 = '#eceff1'; +export const blueGrey100 = '#cfd8dc'; +export const blueGrey200 = '#b0bec5'; +export const blueGrey300 = '#90a4ae'; +export const blueGrey400 = '#78909c'; +export const blueGrey500 = '#607d8b'; +export const blueGrey600 = '#546e7a'; +export const blueGrey700 = '#455a64'; +export const blueGrey800 = '#37474f'; +export const blueGrey900 = '#263238'; + +export const grey50 = '#fafafa'; +export const grey100 = '#f5f5f5'; +export const grey200 = '#eeeeee'; +export const grey300 = '#e0e0e0'; +export const grey400 = '#bdbdbd'; +export const grey500 = '#9e9e9e'; +export const grey600 = '#757575'; +export const grey700 = '#616161'; +export const grey800 = '#424242'; +export const grey900 = '#212121'; + +export const black = '#000000'; +export const white = '#ffffff'; + +export const transparent = 'rgba(0, 0, 0, 0)'; +export const fullBlack = 'rgba(0, 0, 0, 1)'; +export const darkBlack = 'rgba(0, 0, 0, 0.87)'; +export const lightBlack = 'rgba(0, 0, 0, 0.54)'; +export const minBlack = 'rgba(0, 0, 0, 0.26)'; +export const faintBlack = 'rgba(0, 0, 0, 0.12)'; +export const fullWhite = 'rgba(255, 255, 255, 1)'; +export const darkWhite = 'rgba(255, 255, 255, 0.87)'; +export const lightWhite = 'rgba(255, 255, 255, 0.54)'; \ No newline at end of file diff --git a/ui/app/lib/common.js b/ui/app/lib/common.js new file mode 100644 index 00000000..14a30c32 --- /dev/null +++ b/ui/app/lib/common.js @@ -0,0 +1,7 @@ +/* + * pollute the global namespace with some useful utils + */ + +import React from 'react' + +window.elem = React.createElement \ No newline at end of file diff --git a/ui/app/lib/validate.js b/ui/app/lib/validate.js new file mode 100644 index 00000000..4e672e8f --- /dev/null +++ b/ui/app/lib/validate.js @@ -0,0 +1,82 @@ +// https://github.com/dropbox/zxcvbn + +var DOMAIN_RE = /^((?:(?:(?:\w[\.\-\+]?)*)\w)+)((?:(?:(?:\w[\.\-\+]?){0,62})\w)+)\.(\w{2,6})$/ +var USER_RE = /^[a-z0-9_-]{1,200}$/ +var USER_INT_RE = /^[\.@a-z0-9_-]*$/ + +// +// Validations returns an error message or false if no errors +// + +class Validations { + + domain(input) { + if (!input.match(DOMAIN_RE)) { + return "Not a domain name" + } else { + return null + } + } + + usernameInteractive(input) { + if (!input.match(USER_INT_RE)) { + return "Username contains an invalid character" + } + return false + } + + username(input) { + if (!input.match(USER_INT_RE)) { + return "Username contains an invalid character" + } + if (!input.match('@')) { + return "Username must be in the form username@domain" + } + let parts = input.split('@') + let userpart = parts[0] + let domainpart = parts[1] + if (!userpart.match(USER_RE)) { + return "Username contains an invalid character" + } else if (!domainpart.match(DOMAIN_RE)) { + return "Username must include a valid domain name." + } + return false + } + + passwordStrength(passwd) { + if (typeof(zxcvbn) == 'function') { + // zxcvbn performs very slow on long strings, so we cap + // the calculation at 30 characters + return zxcvbn(passwd.substring(0,30)) + } else { + return null + } + } + + // + // loads the zxcvbn library. because this library is big, we don't load it + // every time, just when needed. + // + // this is the webpack way to do this: + // + // require.ensure([], function () { + // var zxcvbn = require('zxcvbn'); + // }); + // + // that works, but requires that we also process the original coffeescript + // source if we want to avoid warning messages. + // + loadPasswdLib(onload) { + var id = "zxcvbn-script" + if (!document.getElementById(id)) { + var script = document.createElement('script') + script.id = id + script.onload = onload + script.src = './js/zxcvbn.js' + document.getElementsByTagName('script')[0].appendChild(script) + } + } +} + +var Validate = new Validations() +export default Validate diff --git a/ui/app/main.js b/ui/app/main.js new file mode 100644 index 00000000..b1628953 --- /dev/null +++ b/ui/app/main.js @@ -0,0 +1,26 @@ +// +// main entry point for app execution +// +// This is determined by the 'entry' option in webpack.config.js +// + +import React from 'react' +import ReactDOM from 'react-dom' + +import PanelSwitcher from 'components/panel_switcher' +import App from 'app' + +class Main extends React.Component { + render() { + return React.createElement(PanelSwitcher) + } + + componentDidMount() { + App.start() + } +} + +ReactDOM.render( + React.createElement(Main), + document.getElementById('app') +) diff --git a/ui/app/models/account.js b/ui/app/models/account.js new file mode 100644 index 00000000..52fea93d --- /dev/null +++ b/ui/app/models/account.js @@ -0,0 +1,143 @@ +// +// An account is an abstraction of a user and a provider. +// The user part is optional, so an Account might just represent a provider. +// + +import bitmask from 'lib/bitmask' + +export default class Account { + + constructor(address, props={}) { + this.address = address + this._authenticated = props.authenticated + } + + // + // currently, bitmask.js uses address for id, so we return address here too. + // also, we don't know uuid until after authentication. + // + // TODO: change to uuid when possible. + // + get id() { + return this._address + } + + get domain() { + return this._address.split('@')[1] + } + + get address() { + return this._address + } + + set address(address) { + if (!address.match('@')) { + this._address = '@' + address + } else { + this._address = address + } + } + + get userpart() { + return this._address.split('@')[0] + } + + get authenticated() { + return this._authenticated + } + + get hasEmail() { + return true + } + + // + // returns a promise, fulfill is passed account object + // + login(password) { + return bitmask.bonafide.user.auth(this.address, password).then( + response => { + if (response.uuid) { + this._uuid = response.uuid + this._authenticated = true + } + return this + } + ) + } + + // + // returns a promise, fulfill is passed account object + // + logout() { + return bitmask.bonafide.user.logout(this.id).then( + response => { + this._authenticated = false + this._address = '@' + this.domain + return this + } + ) + } + + // + // returns the matching account in the list of accounts, or adds it + // if it is not already present. + // + static find(address) { + // search by full address + let account = Account.list.find(i => { + return i.address == address + }) + // failing that, search by domain + if (!account) { + let domain = '@' + address.split('@')[1] + account = Account.list.find(i => { + return i.address == domain + }) + if (account) { + account.address = address + } + } + // failing that, create new account + if (!account) { + account = new Account(address) + Account.list.push(account) + } + return account + } + + // + // returns a promise, fullfill is passed account object + // + static active() { + return bitmask.bonafide.user.active().then( + response => { + if (response.user == '') { + return null + } else { + return new Account(response.user, {authenticated: true}) + } + } + ) + } + + static add(account) { + if (!Account.list.find(i => {return i.id == account.id})) { + Account.list.push(account) + } + } + + static remove(account) { + Account.list = Account.list.filter(i => { + return i.id != account.id + }) + } + // return Account.list + // return new Promise(function(resolve, reject) { + // window.setTimeout(function() { + // resolve(['@blah', '@lala']) + // }, 1000) + // }) + // } +} + +Account.list = [] diff --git a/ui/app/models/dummy_account.js b/ui/app/models/dummy_account.js new file mode 100644 index 00000000..99fb6623 --- /dev/null +++ b/ui/app/models/dummy_account.js @@ -0,0 +1,33 @@ +// +// A proxy of an account, but with a different ID. For testing. +// + +import bitmask from 'lib/bitmask' + +export default class DummyAccount { + + constructor(account) { + this.account = account + } + + get id() { + return 'dummy--' + this.account.address + } + + get domain() {return this.account.domain} + get address() {return this.account.address} + get userpart() {return this.account.userpart} + get authenticated() {return this.account.authenticated} + get hasEmail() {return this.account.hasEmail} + login(password) {return this.account.login(password)} + + logout() { + return bitmask.bonafide.user.logout(this.address).then( + response => { + this._authenticated = false + this._address = '@' + this.domain + return this + } + ) + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 00000000..d491edd8 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,34 @@ +{ + "name": "bitmask_js", + "version": "0.0.1", + "description": "bitmask user interface in javascript", + "license": "GPL-3.0", + "homepage": "https://bitmask.net", + "repository": "https://leap.se/git/bitmask_client.git", + "dependencies": {}, + "devDependencies": { + "babel": "^6.5.2", + "babel-loader": "^6.2.4", + "babel-polyfill": "^6.13.0", + "babel-preset-es2015": "^6.9.0", + "babel-preset-react": "^6.11.1", + "babel-preset-stage-0": "^6.5.0", + "bootstrap": "^3.3.7", + "copy-webpack-plugin": "^3.0.1", + "css-loader": "^0.23.1", + "less": "^2.7.1", + "less-loader": "^2.2.3", + "react": "^15.2.1", + "react-bootstrap": "^0.30.2", + "react-dom": "^15.2.1", + "style-loader": "^0.13.1", + "webpack": "^1.13.1", + "zxcvbn": "^4.3.0" + }, + "scripts": { + "open": "gnome-open http://localhost:7070", + "watch": "NODE_ENV=development webpack --watch", + "build": "NODE_ENV=development webpack", + "build:production": "NODE_ENV=production webpack" + } +} diff --git a/ui/pydist/README.md b/ui/pydist/README.md new file mode 100644 index 00000000..6e2b11f6 --- /dev/null +++ b/ui/pydist/README.md @@ -0,0 +1,4 @@ +This directory holds the python package of the javascript app, called 'bitmask_js'. + +Why it it done this way? By creating a python package, it is easier for the +javascript app to be distributed with the bitmask client. diff --git a/ui/pydist/setup.py b/ui/pydist/setup.py new file mode 100644 index 00000000..b5d3ffb1 --- /dev/null +++ b/ui/pydist/setup.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# setup.py +# Copyright (C) 2016 LEAP Encryption Acess Project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Setup file for bitmask_js +""" + +from setuptools import setup +import datetime +import time + +long_description = \ +'''bitmask_js +----------------- +This package contains the already compiled javascript resources for the bitmask UI. + +If you want to develop for this UI, please checkout the bitmask-dev [0] repo and follow the instructions in the ui/README.md file. + +[0] https://github.com/leapcode/bitmask-dev''' + +now = datetime.datetime.now() +timestamp = time.strftime('%Y%m%d%H%M', now.timetuple()) + +setup( + name='bitmask_js', + version='0.1.%s' % timestamp, + description='Bitmask UI', + long_description=long_description, + author='LEAP Encrypted Access Project', + author_email='info@leap.se', + url='http://leap.se', + packages=['bitmask_js'], + package_data={ + '': ['public/*', + 'public/css/*', + 'public/fonts/*', + 'public/img/*', + 'publlic/js/*', + ] + } +) diff --git a/ui/webpack.config.js b/ui/webpack.config.js new file mode 100644 index 00000000..c0f8d191 --- /dev/null +++ b/ui/webpack.config.js @@ -0,0 +1,84 @@ +var path = require('path') +var webpack = require('webpack') +var CopyWebpackPlugin = require('copy-webpack-plugin'); + +var config = { + context: path.join(__dirname, 'app'), + entry: ['babel-polyfill', './main.js'], + output: { + path: path.join(__dirname, 'pydist', 'bitmask_js', 'public'), + filename: 'app.bundle.js' + }, + resolve: { + modulesDirectories: ['node_modules', './app'], + extensions: ['', '.js', '.jsx'] + }, + module: { + loaders: [ + // babel transform + { + test: /\.js$/, + loader: 'babel-loader', + exclude: /node_modules/, + query: { + presets: ['react', 'es2015'] + } + }, + { + test: /\.css$/, + loader: "style!css" + }, + { + test: /\.less$/, + loader: "style!css!less?noIeCompat" + } + ] + }, + plugins: [ + // don't bundle when there is an error: + new webpack.NoErrorsPlugin(), + + // https://webpack.github.io/docs/code-splitting.html + // new webpack.optimize.CommonChunkPlugin('common.js') + + // + // ASSETS + // + // If you make changes to the asset files, you will need to stop then rerun + // `npm run watch` for the changes to take effect. + // + // For more information: https://github.com/kevlened/copy-webpack-plugin + // + new CopyWebpackPlugin([ + { from: 'css/*.css' }, + { from: 'img/*'}, + { from: 'index.html' }, + { from: '../node_modules/bootstrap/dist/css/bootstrap.min.css', to: 'css' }, + { from: '../node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff', to: 'fonts' }, + { from: '../node_modules/zxcvbn/dist/zxcvbn.js', to: 'js' } + ]) + ], + stats: { + colors: true + }, + // source-map can be used in production or development + // but it creates a separate file. + devtool: 'source-map' +} + +/* +if (process.env.NODE_ENV == 'production') { + // see https://github.com/webpack/docs/wiki/optimization + config.plugins.push( + new webpack.optimize.UglifyJsPlugin({ + compress: { warnings: false }, + output: { comments: false } + }), + new webpack.optimize.DedupePlugin() + ) +} else { + config.devtool = 'inline-source-map'; +} +*/ + +module.exports = config diff --git a/www/Makefile b/www/Makefile deleted file mode 100644 index d67a02b3..00000000 --- a/www/Makefile +++ /dev/null @@ -1,47 +0,0 @@ -# -# builds for development mode -# - -dev-build: build-clean - npm install - npm run build - touch pydist/bitmask_js/__init__.py - -dev-install: dev-build - pip install -e pydist - -# -# installs python package, but does not rebuild the js. -# for usage when you don't want to install nodejs -# -dev-install-prebuilt: - pip install -e pydist - - -# -# distribution builds -# - -dist-build: build-clean - npm install - npm run build:production - touch pydist/bitmask_js/__init__.py - cd pydist && python setup.py bdist_wheel - -dist-install: dist-build - pip install pydist/dist/*.whl - -# -# cleaning up -# - -build-clean: - rm -rf pydist/bitmask_js - rm -rf pydist/dist - rm -rf pydist/build - -clean: build-clean - rm -rf node_modules - -uninstall: - pip uninstall bitmask_js diff --git a/www/README.md b/www/README.md deleted file mode 100644 index 3f276c00..00000000 --- a/www/README.md +++ /dev/null @@ -1,112 +0,0 @@ -Bitmask Javascript UI -================================================================= - -Here lies the user interface for Bitmask, written in Javascript. - -quick start: - - sudo apt install nodejs npm nodejs-legacy - npm install # installs development dependencies in "node_modules" - npm run watch # continually rebuilds source .js into "pydist" - -build for deployment: - - npm install - npm run build:production - -After 'build', 'build:production', or 'watch' is run, everything needed for -Bitmask JS is contained in the 'pydist' directory. No additional files are -needed. Open the pydist/bitmask_js/public/index.html file in a browser or web -widget. - -However! Because of the single origin policy of browsers, you will need to -open public/index.html through the webserver included with bitmaskd (e.g. -http://localhost:7070) - -In order for this JS app to be loaded by bitmask, it must be packaged as a -python package and installed in the virtualenv: - - source path-to-virtualenv/bin/activate - make dev-install # builds and installs JS app as python package - pkill bitmaskd # make sure bitmaskd is not already running - bitmaskd # launch backend - npm run open # opens http://localhost:7070/ in a browser - npm run watch # rebuild JS whenever source file is changed. - -In order to package for distribution: - - make dist-build - -NOTE: If you make changes to the asset files, like add or modify an image, you - will need to stop then rerun `npm run watch` for the changes to take - effect. - -Development Dependencies ------------------------------------------------------------------ - -This application has no "runtime" dependencies: all the javascript needed is -bundled and included. However, there are many development dependencies. -Run `npm ls` for a full list. - -**npm** - -Package management, controlled by the package.json file. - -**webpack** - -Asset bundling and transformation. It takes all your javascript and CSS and -bundles it into one (or more) files, handling support for 'require' and scope -separation. - -loaders & plugins: - -* babel-loader: use Babel with Webpack. - -* style-loader/css-loader: standard css loader. - -* less-loader: allows use the less stylesheet. - -* copy-webpack-plugin: allows us to specify what files to copy using Webpack. - -* extract-text-webpack-plugin: allows you to split js and css into separate - files. - -**babel** - -Babel is used to compile javascript into javascript, transforming along the -way. We have enabled these plugins: - -* babel-presets-react: Adds support for React features, such as JSX (html - inlined in js files). - -* babel-presets-es2015: Allows the use of modern ES6 javascript. - -* babel-presets-stage-0: Allows the use of some ES7 proposals, even though - these are not standardized yet. Makes classes nicer. - -* babel-polyfill: This is not part of the babel transpiling, but is distributed by babel. This polyfill will give you a full ES2015 environment even if the browser is missing some javascript features. We include this in the 'entry' option of the webpack config. https://babeljs.io/docs/usage/polyfill/ - -**react** - -React is an efficient way to generate HTML views with Javascript. It allows you -to create interactive UIs without ever modifying the DOM by using "one way" -data binding. This greatly simplifies the code and reduces errors. - -**bootstrap** - -The world's most popular CSS styles for UI elements. The npm package includes -both pre-compiled css and less stylesheets. Even though Semantic UI is better, -Bootstrap components for React are much more stable, I have found, and also are -easy to theme. - -To integrate Bootstrap with React: - -* react-bootstrap: React components that use Bootstrap styles. These component - include all the needed javascript and don't require JQuery, although they do - require that the bootstrap CSS is loaded independently. - -**zxcvbn** - -A password strength checker that doesn't suck, but which is big. This JS is -only loaded when we think we are about to need it. - diff --git a/www/app/app.js b/www/app/app.js deleted file mode 100644 index 57120a4e..00000000 --- a/www/app/app.js +++ /dev/null @@ -1,33 +0,0 @@ -import bitmask from 'lib/bitmask' -import Account from 'models/account' - -class Application { - constructor() { - } - - // - // main entry point for the application - // - start() { - Account.active().then(account => { - if (account == null) { - this.show('greeter', {onLogin: this.onLogin.bind(this)}) - } else { - this.show('main', {initialAccount: account}) - } - }, error => { - this.show('error', {error: error}) - }) - } - - onLogin(account) { - this.show('main', {initialAccount: account}) - } - - show(panel, properties) { - this.switcher.show(panel, properties) - } -} - -var App = new Application -export default App \ No newline at end of file diff --git a/www/app/components/area.js b/www/app/components/area.js deleted file mode 100644 index e903e5f5..00000000 --- a/www/app/components/area.js +++ /dev/null @@ -1,65 +0,0 @@ -// -// A bootstrap panel, but with some extra options -// - -import React from 'react' -// import {Panel} from 'react-bootstrap' - -class Area extends React.Component { - - static get defaultProps() {return{ - position: null, // top or bottom - size: 'small', // small or big - type: null, // light or dark - className: null - }} - - constructor(props) { - super(props) - } - - render() { - let style = {} - let innerstyle = {} - if (this.props.position == 'top') { - style.borderBottomRightRadius = '0px' - style.borderBottomLeftRadius = '0px' - style.marginBottom = '0px' - style.borderBottom = '0px' - if (this.props.size == 'big') { - innerstyle.padding = '25px' - } - } else if (this.props.position == 'bottom') { - style.borderTopRightRadius = '0px' - style.borderTopLeftRadius = '0px' - style.borderTop = '0px' - if (this.props.size == 'big') { - innerstyle.padding = '15px 25px' - } - } - - let type = this.props.type ? "area-" + this.props.type : "" - let className = ['panel', 'panel-default', type, this.props.className].join(' ') - return( -
    -
    - {this.props.children} -
    -
    - ) - } - -} - -// Area.propTypes = { -// children: React.PropTypes.oneOfType([ -// React.PropTypes.element, -// React.PropTypes.arrayOf(React.PropTypes.element) -// ]) -// } - -//Area.propTypes = { -// children: React.PropTypes.element.isRequired -//} - -export default Area diff --git a/www/app/components/center.js b/www/app/components/center.js deleted file mode 100644 index 6fa62128..00000000 --- a/www/app/components/center.js +++ /dev/null @@ -1,39 +0,0 @@ -// -// puts a block right in the center of the window -// - -import React from 'react' - -class Center extends React.Component { - - static get defaultProps() {return{ - width: null - }} - - constructor(props) { - super(props) - } - - render() { - let style = null - if (this.props.width) { - style = {width: this.props.width + 'px'} - } - return ( -
    -
    - {this.props.children} -
    -
    - ) - } -} - -Center.propTypes = { - children: React.PropTypes.oneOfType([ - React.PropTypes.element, - React.PropTypes.arrayOf(React.PropTypes.element) - ]) -} - -export default Center diff --git a/www/app/components/debug_panel.js b/www/app/components/debug_panel.js deleted file mode 100644 index 7515ba84..00000000 --- a/www/app/components/debug_panel.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react' -import App from '../app' - - -class DebugPanel extends React.Component { - - constructor(props) { - super(props) - this.click = this.click.bind(this) - } - - componentDidMount() { - this.click(window.location.hash.replace('#', '')) - } - - click(panel_name) { - window.location.hash = panel_name - App.show(panel_name) - } - - panel(panel_name) { - return elem( - 'a', - { onClick: () => this.click(panel_name), key: panel_name }, - panel_name - ) - } - - render() { - return elem('div', {className: 'debug-panel'}, - this.panel('splash'), - this.panel('greeter'), - this.panel('wizard'), - this.panel('main') - ) - } - -} - -export default DebugPanel \ No newline at end of file diff --git a/www/app/components/error_panel.js b/www/app/components/error_panel.js deleted file mode 100644 index fc88d459..00000000 --- a/www/app/components/error_panel.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react' -import Center from './center' -import Area from './area' - -export default class ErrorPanel extends React.Component { - - constructor(props) { - super(props) - } - - render () { - return ( -
    - -

    Error

    - {this.props.error} - -
    - ) - } -} diff --git a/www/app/components/greeter_panel.js b/www/app/components/greeter_panel.js deleted file mode 100644 index 4552db18..00000000 --- a/www/app/components/greeter_panel.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import Login from './login' -import Center from './center' -import Splash from './splash' -import Area from './area' -import { Glyphicon } from 'react-bootstrap' -import App from 'app' - -export default class GreeterPanel extends React.Component { - - constructor(props) { - super(props) - } - - newAccount() { - App.show('wizard') - } - - render () { - return
    - -
    - - - - - -   - Create a new account... - -
    -
    - } -} diff --git a/www/app/components/list_edit.js b/www/app/components/list_edit.js deleted file mode 100644 index 0d557d22..00000000 --- a/www/app/components/list_edit.js +++ /dev/null @@ -1,122 +0,0 @@ -// -// A simple list of items, with minus and plus buttons to add and remove -// items. -// - -import React from 'react' -import {Button, ButtonGroup, ButtonToolbar, Glyphicon, FormControl} from 'react-bootstrap' - -const CONTAINER_CSS = { - display: "flex", - flexDirection: "column" -} -const SELECT_CSS = { - padding: "0px", - flex: "1 1 1000px", - overflowY: "scroll" -} -const OPTION_CSS = { - padding: "10px" -} -const TOOLBAR_CSS = { - paddingTop: "10px", - flex: "0 0 auto" -} - -class ListEdit extends React.Component { - - static get defaultProps() {return{ - width: null, - items: [ - 'aaaaaaa', - 'bbbbbbb', - 'ccccccc' - ], - selected: null, - onRemove: null, - onAdd: null, - }} - - constructor(props) { - super(props) - let index = 0 - if (props.selected) { - index = props.items.indexOf(props.selected) - } - this.state = { - selected: index - } - this.click = this.click.bind(this) - this.add = this.add.bind(this) - this.remove = this.remove.bind(this) - } - - setSelected(index) { - this.setState({ - selected: index - }) - } - - click(e) { - let row = parseInt(e.target.value) - if (row >= 0) { - this.setState({selected: row}) - } - } - - add() { - if (this.props.onAdd) { - this.props.onAdd() - } - } - - remove() { - if (this.state.selected >= 0 && this.props.onRemove) { - if (this.props.items.length == this.state.selected + 1) { - // if we remove the last item, set the selected item - // to the one right before it. - this.setState({selected: (this.state.selected - 1)}) - } - this.props.onRemove(this.props.items[this.state.selected]) - } - } - - render() { - let options = null - if (this.props.items) { - options = this.props.items.map((item, i) => { - return - }, this) - } - return( -
    - - {options} - - - - - - - -
    - ) - } - -} - -ListEdit.propTypes = { - children: React.PropTypes.oneOfType([ - React.PropTypes.element, - React.PropTypes.arrayOf(React.PropTypes.element) - ]) -} - -export default ListEdit diff --git a/www/app/components/login.js b/www/app/components/login.js deleted file mode 100644 index fe4ef5b2..00000000 --- a/www/app/components/login.js +++ /dev/null @@ -1,302 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom' - -import { FormGroup, ControlLabel, FormControl, HelpBlock, Button, - Checkbox, Glyphicon, Overlay, Tooltip, Alert } from 'react-bootstrap' -import Spinner from './spinner' - -import Validate from 'lib/validate' -import App from 'app' -import Account from 'models/account' - -class Login extends React.Component { - - static get defaultProps() {return{ - rememberAllowed: false, // if set, show remember password checkbox - domain: null, // if set, only allow this domain - onLogin: null - }} - - constructor(props) { - super(props) - - // validation states can be null, 'success', 'warning', or 'error' - - this.state = { - loading: false, - - authError: false, // authentication error message - - username: "etest1@riseup.net", - usernameState: null, // username validation state - usernameError: false, // username help message - - password: "whatever", - passwordState: null, // password validation state - passwordError: false, // password help message - - disabled: false, - remember: false // remember is checked? - } - - // prebind: - this.onUsernameChange = this.onUsernameChange.bind(this) - this.onUsernameBlur = this.onUsernameBlur.bind(this) - this.onPassword = this.onPassword.bind(this) - this.onSubmit = this.onSubmit.bind(this) - this.onRemember = this.onRemember.bind(this) - } - - componentDidMount() { - Validate.loadPasswdLib() - } - - render () { - let rememberCheck = "" - let submitButton = "" - let usernameHelp = null - let passwordHelp = null - let message = null - - if (this.props.rememberAllowed) { - let props = { - style: {marginTop: "0px"}, - onChange: this.onRemember - } - - if (this.state.remember) { - rememberCheck = - Remember username and password - - } else { - rememberCheck = - Remember username and password - - } - } - - if (this.state.authError) { - // style may be: success, warning, danger, info - message = ( - {this.state.authError} - ) - } - - if (this.state.usernameError) { - usernameHelp = {this.state.usernameError} - // let props = {shouldUpdatePosition: true, show:true, placement:"right", - // target:this.refs.username} - // usernameHelp = ( - // - // {this.state.usernameError} - // - // ) - } else { - //usernameHelp =   - } - - if (this.state.passwordError) { - passwordHelp = {this.state.passwordError} - // let props = {shouldUpdatePosition: true, show:true, placement:"right", - // target:this.refs.password, component: {this}} - // passwordHelp = ( - // - // {this.state.passwordError} - // - // ) - } else { - //passwordHelp =   - } - - let buttonProps = { - type: "button", - onClick: this.onSubmit, - disabled: !this.maySubmit() - } - if (this.state.loading) { - submitButton = - } else { - submitButton = - } - - let usernameref = null - if (this.props.domain) { - usernameref = function(c) { - if (c != null) { - let textarea = ReactDOM.findDOMNode(c) - let start = textarea.value.indexOf('@') - if (textarea.selectionStart > start) { - textarea.setSelectionRange(start, start) - } - } - } - } - - let form =
    - {message} - - Username - - {this.state.usernameState == 'success' ? null : } - {usernameHelp} - - - - Password - - {this.state.passwordState == 'success' ? null : } - {passwordHelp} - - - {submitButton} - {rememberCheck} -
    - - return form - } - - // - // Here we do a partial validation, because the user has not stopped typing. - // - onUsernameChange(e) { - let username = e.target.value.toLowerCase().replace("\n", "") - if (this.props.domain) { - let [userpart, domainpart] = username.split( - new RegExp('@|' + this.props.domain.replace(".", "\\.") + '$') - ) - username = [userpart, this.props.domain].join('@') - } - let error = Validate.usernameInteractive(username, this.props.domain) - let state = null - if (error) { - state = 'error' - } else { - if (username && username.length > 0) { - let finalError = Validate.username(username) - state = finalError ? null : 'success' - } - } - this.setState({ - username: username, - usernameState: state, - usernameError: error ? error : null - }) - } - - // - // Here we do a more complete validation, since the user have left the field. - // - onUsernameBlur(e) { - let username = e.target.value.toLowerCase() - this.setState({ - username: username - }) - if (username.length > 0) { - this.validateUsername(username) - } else { - this.setState({ - usernameState: null, - usernameError: null - }) - } - } - - onPassword(e) { - let password = e.target.value - this.setState({password: password}) - if (password.length > 0) { - this.validatePassword(password) - } else { - this.setState({ - passwordState: null, - passwordError: null - }) - } - } - - onRemember(e) { - let currentValue = e.target.value == 'on' ? true : false - let value = !currentValue - this.setState({remember: value}) - } - - validateUsername(username) { - let error = Validate.username(username, this.props.domain) - this.setState({ - usernameState: error ? 'error' : 'success', - usernameError: error ? error : null - }) - } - - validatePassword(password) { - let state = null - let message = null - let result = Validate.passwordStrength(password) - if (result) { - message = "Time to crack: " + result.crack_times_display.offline_slow_hashing_1e4_per_second - if (result.score == 0) { - state = 'error' - } else if (result.score == 1 || result.score == 2) { - state = 'warning' - } else { - state = 'success' - } - } - this.setState({ - passwordState: state, - passwordError: message - }) - } - - maySubmit() { - return( - !this.stateLoading && - !this.state.usernameError && - this.state.username != "" && - this.state.password != "" - ) - } - - onSubmit(e) { - e.preventDefault() // don't reload the page please! - if (!this.maySubmit()) { return } - this.setState({loading: true}) - - let account = Account.find(this.state.username) - account.login(this.state.password).then( - account => { - this.setState({loading: false}) - if (this.props.onLogin) { - this.props.onLogin(account) - } - }, - error => { - console.log(error) - if (error == "") { - error = 'Something failed, but we did not get a message' - } - this.setState({ - loading: false, - usernameState: 'error', - passwordState: 'error', - authError: error - }) - } - ) - } - -} - -export default Login \ No newline at end of file diff --git a/www/app/components/main_panel/account_list.js b/www/app/components/main_panel/account_list.js deleted file mode 100644 index d0ef092f..00000000 --- a/www/app/components/main_panel/account_list.js +++ /dev/null @@ -1,113 +0,0 @@ -import React from 'react' -import {Button, ButtonGroup, ButtonToolbar, Glyphicon} from 'react-bootstrap' - -import App from 'app' -import Account from 'models/account' - -export default class AccountList extends React.Component { - - static get defaultProps() {return{ - account: null, - accounts: [], - onAdd: null, - onRemove: null, - onSelect: null - }} - - constructor(props) { - super(props) - - this.state = { - mode: 'expanded' - } - - // prebind: - this.select = this.select.bind(this) - this.add = this.add.bind(this) - this.remove = this.remove.bind(this) - this.expand = this.expand.bind(this) - this.collapse = this.collapse.bind(this) - } - - select(e) { - let account = this.props.accounts.find( - account => account.id == e.currentTarget.dataset.id - ) - if (this.props.onSelect) { - this.props.onSelect(account) - } - } - - add() { - App.show('wizard') - } - - remove() { - } - - expand() { - this.setState({mode: 'expanded'}) - } - - collapse() { - this.setState({mode: 'collapsed'}) - } - - render() { - let style = {} - let expandButton = null - let plusminusButtons = null - - if (this.state.mode == 'expanded') { - expandButton = ( - - ) - plusminusButtons = ( - - - - - ) - } else { - style.width = '60px' - expandButton = ( - - ) - } - - let items = this.props.accounts.map((account, i) => { - let className = account == this.props.account ? 'active' : 'inactive' - return ( -
  • - {account.userpart} - {account.domain} - - -
  • - ) - }) - - - return ( -
    -
      - {items} -
    - - {plusminusButtons} - {expandButton} - -
    - ) - } - - -} diff --git a/www/app/components/main_panel/email_section.js b/www/app/components/main_panel/email_section.js deleted file mode 100644 index a6525d92..00000000 --- a/www/app/components/main_panel/email_section.js +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react' -//import { Button, Glyphicon, Alert } from 'react-bootstrap' -import SectionLayout from './section_layout' -import Account from 'models/account' -import Spinner from 'components/spinner' -import bitmask from 'lib/bitmask' - -export default class EmailSection extends React.Component { - - static get defaultProps() {return{ - account: null - }} - - constructor(props) { - super(props) - this.state = { - status: null - } - this.openKeys = this.openKeys.bind(this) - this.openApp = this.openApp.bind(this) - this.openPrefs = this.openPrefs.bind(this) - - console.log('email constructor') - } - - openKeys() {} - openApp() {} - openPrefs() {} - - render () { - //let message = null - //if (this.state.error) { - // // style may be: success, warning, danger, info - // message = ( - // {this.state.error} - // ) - //} - let button = null - if (this.state.status == 'ready') { - button = - } - return ( - -

    inbox:

    -
    - ) - } -} diff --git a/www/app/components/main_panel/index.js b/www/app/components/main_panel/index.js deleted file mode 100644 index 3cc6c11f..00000000 --- a/www/app/components/main_panel/index.js +++ /dev/null @@ -1,80 +0,0 @@ -// -// The main panel manages the current account and the list of available accounts -// -// It displays multiple sections, one for each service. -// - -import React from 'react' -import App from 'app' -import Login from 'components/login' -import Account from 'models/account' -import DummyAccount from 'models/dummy_account' - -import './main_panel.less' -import AccountList from './account_list' -import UserSection from './user_section' -import EmailSection from './email_section' - -export default class MainPanel extends React.Component { - - static get defaultProps() {return{ - initialAccount: null - }} - - constructor(props) { - super(props) - this.state = { - account: null, - accounts: [] - } - this.activateAccount = this.activateAccount.bind(this) - } - - componentWillMount() { - if (this.props.initialAccount) { - console.log(Account.list) - Account.add(this.props.initialAccount) - Account.add(new DummyAccount(this.props.initialAccount)) - this.setState({ - account: this.props.initialAccount, - accounts: Account.list - }) - } - } - - activateAccount(account) { - this.setState({ - account: account, - accounts: Account.list - }) - } - - //setAccounts(accounts) { - // this.setState({ - // accounts: accounts - // }) - //} - - render() { - let emailSection = null - let vpnSection = null - - if (this.state.account.authenticated) { - if (this.state.account.hasEmail) { - emailSection = - } - } - - return ( -
    - -
    - - {vpnSection} - {emailSection} -
    -
    - ) - } - -} diff --git a/www/app/components/main_panel/main_panel.less b/www/app/components/main_panel/main_panel.less deleted file mode 100644 index 4e0ecb05..00000000 --- a/www/app/components/main_panel/main_panel.less +++ /dev/null @@ -1,212 +0,0 @@ -// The space around account entries: -@accounts-padding: 8px; -@accounts-corner: 6px; -@accounts-width: 200px; - -// -// LAYOUT -// - -.main-panel { - position: absolute; - height: 100%; - width: 100%; - display: -webkit-flex; - -webkit-flex-direction: row; - - > .body { - -webkit-flex: 1 1 auto; - overflow: auto; - } - - .accounts { - -webkit-flex: 0 0 auto; - overflow-y: auto; - overflow-x: hidden; - - display: -webkit-flex; - -webkit-flex-direction: column; - ul { - -webkit-flex: 1 1 1000px; - } - .btn-toolbar { - -webkit-flex: 0 0 auto; - } - - } - -} - -// -// Style -// - - -.main-panel > .body { - padding: 20px; -} - -.main-panel .accounts { - background-color: #333; - width: @accounts-width; - padding: @accounts-padding; - padding-right: 0px; -} - -.main-panel .accounts ul { - list-style: none; - margin: 0; - padding: 0; -} - -.main-panel .accounts li { - position: relative; - cursor: pointer; - color: white; - padding: 15px; - background-color: #444; - margin-bottom: @accounts-padding; - border-top-left-radius: @accounts-corner - 1; - border-bottom-left-radius: @accounts-corner - 1; - z-index: 100; -} - -.main-panel .accounts li span.domain { - display: block; - font-weight: bold; - //margin-left: 40px; -} - -.main-panel .accounts li span.username { - display: block; - //margin-left: 40px; - //line-height: 7px; - //margin-bottom: 4px; -} - -/*.main-panel .accounts li span.icon { - display: block; - height: 32px; - width: 32px; - background-color: #999; - float: left; -} -*/ - -.main-panel .accounts li.active { - background-color: white; - color: #333; -} - -.main-panel .accounts li.active span.arc { - display: block; - height: @accounts-corner; - width: @accounts-corner; - background-color: white; - position: absolute; - right: 0; -} - -.main-panel .accounts li.active span.arc.top { - top: 0; - margin-top: -@accounts-corner; -} -.main-panel .accounts li.active span.arc.bottom { - bottom: 0; - margin-bottom: -@accounts-corner; -} -.main-panel .accounts li.active span.arc:after { - display: block; - content: ""; - border-radius: 100%; - height: 0px; - width: 0px; - margin-left: -@accounts-corner; -} -.main-panel .accounts li.active span.arc.top:after { - border: @accounts-corner solid transparent; - border-right: @accounts-corner solid #333; - margin-top: -@accounts-corner; - -webkit-transform: rotate(45deg); - transform: rotate(45deg); -} -.main-panel .accounts li.active span.arc.bottom:after { - border: @accounts-corner solid #333; -} - -.main-panel .accounts .btn.expander { - margin-right: @accounts-padding; -} - -// -// SECTIONS -// - -@icon-size: 32px; -@status-size: 24px; -@section-padding: 10px; - -// service sections layout - -.main-panel .service-section { - display: -webkit-flex; - -webkit-flex-direction: row; - > .icon { - -webkit-flex: 0 0 auto; - } - > .body { - -webkit-flex: 1 1 auto; - } - > .buttons { - -webkit-flex: 0 0 auto; - } - > .status { - -webkit-flex: 0 0 auto; - display: -webkit-flex; - -webkit-align-items: center; - } - -} - -.main-panel .service-section div { - //outline: 1px solid rgba(0,0,0,0.1); -} - -// service sections style - -.main-panel .service-section { - background: #f6f6f6; - border-radius: 4px; - padding: 10px; - margin-bottom: 10px; - &.wide-margin { - padding: 20px 20px 20px 10px; // arbitrary, looks nice - } - > .icon { - padding-right: @section-padding; - img { - width: @icon-size; - height: @icon-size; - } - } - > .body { - h1 { - margin: 0; - padding: 0; - font-size: @icon-size - 10; - line-height: @icon-size; - } - } - > .buttons { - padding-left: 10px; - } - > .status { - padding-left: @section-padding; - width: @section-padding + @status-size; - img { - width: @status-size; - height: @status-size; - } - } -} - diff --git a/www/app/components/main_panel/section_layout.js b/www/app/components/main_panel/section_layout.js deleted file mode 100644 index e7c6f2ab..00000000 --- a/www/app/components/main_panel/section_layout.js +++ /dev/null @@ -1,59 +0,0 @@ -// -// This is the layout for a service section in the main window. -// It does not do anything except for arrange items using css and html. -// - -import React from 'react' - -export default class SectionLayout extends React.Component { - - static get defaultProps() {return{ - icon: null, - buttons: null, - status: null, - className: "", - style: {} - }} - - constructor(props) { - super(props) - } - - render() { - let className = ["service-section", this.props.className].join(' ') - let status = null - let icon = null - let buttons = null - - if (this.props.status) { - status = ( -
    - -
    - ) - } - if (this.props.icon) { - icon = ( -
    - -
    - ) - } - if (this.props.buttons) - buttons = ( -
    - {this.props.buttons} -
    - ) - return( -
    - {icon} -
    - {this.props.children} -
    - {buttons} - {status} -
    - ) - } -} diff --git a/www/app/components/main_panel/user_section.js b/www/app/components/main_panel/user_section.js deleted file mode 100644 index 0b4ba136..00000000 --- a/www/app/components/main_panel/user_section.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react' -import { Button, Glyphicon, Alert } from 'react-bootstrap' -import SectionLayout from './section_layout' -import Login from 'components/login' -import Spinner from 'components/spinner' -import Account from 'models/account' - -import bitmask from 'lib/bitmask' - -export default class UserSection extends React.Component { - - static get defaultProps() {return{ - account: null, - onLogout: null, - onLogin: null - }} - - constructor(props) { - super(props) - this.state = { - error: null, - loading: false - } - this.logout = this.logout.bind(this) - } - - logout() { - this.setState({loading: true}) - this.props.account.logout().then( - account => { - this.setState({error: null, loading: false}) - if (this.props.onLogout) { - this.props.onLogout(account) - } - }, error => { - this.setState({error: error, loading: false}) - } - ) - } - - render () { - let message = null - if (this.state.error) { - // style may be: success, warning, danger, info - message = ( - {this.state.error} - ) - } - - if (this.props.account.authenticated) { - let button = null - if (this.state.loading) { - button = - } else { - button = - } - return ( - -

    {this.props.account.address}

    - {message} -
    - ) - } else { - return ( - - - - ) - } - } -} diff --git a/www/app/components/main_panel/vpn_section.js b/www/app/components/main_panel/vpn_section.js deleted file mode 100644 index e69de29b..00000000 diff --git a/www/app/components/panel_switcher.js b/www/app/components/panel_switcher.js deleted file mode 100644 index aaf2dc5b..00000000 --- a/www/app/components/panel_switcher.js +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom' - -import DebugPanel from './debug_panel' -import Splash from './splash' -import GreeterPanel from './greeter_panel' -import MainPanel from './main_panel' -import Wizard from './wizard' - -import App from 'app' -import 'lib/common' - -export default class PanelSwitcher extends React.Component { - - constructor(props) { - super(props) - this.state = { - panel: null, - panel_properties: null, - debug: false - } - App.switcher = this - } - - show(component_name, properties={}) { - this.setState({panel: component_name, panel_properties: properties}) - } - - render() { - let elems = [] - if (this.panelExist(this.state.panel)) { - elems.push( - this.panelRender(this.state.panel, this.state.panel_properties) - ) - } - if (this.state.debug) { - elems.push( - elem(DebugPanel, {key: 'debug'}) - ) - } - return
    {elems}
    - } - - panelExist(panel) { - return panel && this['render_'+panel] - } - - panelRender(panel_name, props) { - let panel = this['render_'+panel_name](props) - return elem('div', {key: 'panel'}, panel) - } - - render_splash(props) {return elem(Splash, props)} - render_wizard(props) {return elem(Wizard, props)} - render_greeter(props) {return elem(GreeterPanel, props)} - render_main(props) {return elem(MainPanel, props)} - -} diff --git a/www/app/components/spinner/index.js b/www/app/components/spinner/index.js deleted file mode 100644 index ffc32850..00000000 --- a/www/app/components/spinner/index.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import './spinner.css'; - -class Spinner extends React.Component { - render() { - let props = {} - return
    -
    -
    -
    -
    - } -} - -export default Spinner \ No newline at end of file diff --git a/www/app/components/spinner/spinner.css b/www/app/components/spinner/spinner.css deleted file mode 100644 index 5e8535c9..00000000 --- a/www/app/components/spinner/spinner.css +++ /dev/null @@ -1,42 +0,0 @@ -.spinner { - height: 18px; - display: inline-block; -} - -.spinner > div { - width: 18px; - height: 18px; - background-color: #000; - vertical-align: middle; - border-radius: 100%; - display: inline-block; - -webkit-animation: bouncedelay 1.5s infinite ease-in-out; - animation: bouncedelay 1.5s infinite ease-in-out; - -webkit-animation-fill-mode: both; - animation-fill-mode: both; -} - -.spinner .spin1 { - -webkit-animation-delay: -.46s; - animation-delay: -.46s; -} -.spinner .spin2 { - -webkit-animation-delay: -.24s; - animation-delay: -.24s; -} - -@-webkit-keyframes bouncedelay { - 0%, 80%, 100% { - -webkit-transform: scale(0.5); - } 40% { - -webkit-transform: scale(0.9); - } -} - -@keyframes bouncedelay { - 0%, 80%, 100% { - transform: scale(0.5); - } 40% { - transform: scale(0.9); - } -} diff --git a/www/app/components/splash.js b/www/app/components/splash.js deleted file mode 100644 index 46170d23..00000000 --- a/www/app/components/splash.js +++ /dev/null @@ -1,132 +0,0 @@ -/* - * A simple animated splash screen - */ - -import React from 'react' -import * as COLOR from '../lib/colors' - -const colorList = [ - COLOR.red200, COLOR.pink200, COLOR.purple200, COLOR.deepPurple200, - COLOR.indigo200, COLOR.blue200, COLOR.lightBlue200, COLOR.cyan200, - COLOR.teal200, COLOR.green200, COLOR.lightGreen200, COLOR.lime200, - COLOR.yellow200, COLOR.amber200, COLOR.orange200, COLOR.deepOrange200 -] - -export default class Splash extends React.Component { - - static get defaultProps() {return{ - speed: "fast", - mask: true, - onClick: null - }} - - constructor(props) { - super(props) - this.counter = 0 - this.interval = null - this.ctx = null - this.stepAngle = 0 - this.resize = this.resize.bind(this) - this.click = this.click.bind(this) - if (this.props.speed == "fast") { - this.fps = 30 - this.stepAngle = 0.005 - } else { - this.fps = 30 - this.stepAngle = 0.0005 - } - } - - componentDidMount() { - this.interval = setInterval(this.tick.bind(this), 1000/this.fps) - this.canvas = this.refs.canvas - this.ctx = this.canvas.getContext('2d') - window.addEventListener('resize', this.resize) - } - - componentWillUnmount() { - clearInterval(this.interval) - window.removeEventListener('resize', this.resize) - } - - click() { - if (this.props.onClick) { - this.props.onClick() - } - } - - tick() { - this.counter++ - this.updateCanvas() - } - - resize() { - this.canvas.width = window.innerWidth - this.canvas.height = window.innerHeight - this.updateCanvas() - } - - updateCanvas() { - const arcCount = 16 - const arcAngle = 1 / arcCount - const x = this.canvas.width / 2 - const y = this.canvas.height / 2 - const radius = screen.height + screen.width - - for (let i = 0; i < arcCount; i++) { - let startAngle = Math.PI * 2 * i/arcCount + this.stepAngle*this.counter - let endAngle = Math.PI * 2 * (i+1)/arcCount + this.stepAngle*this.counter - - this.ctx.fillStyle = colorList[i % colorList.length] - this.ctx.strokeStyle = colorList[i % colorList.length] - this.ctx.beginPath() - this.ctx.moveTo(x, y) - this.ctx.arc(x, y, radius, startAngle, endAngle) - this.ctx.lineTo(x, y) - this.ctx.fill() - this.ctx.stroke() - } - - } - - render () { - let overlay = null - let mask = null - if (this.props.onClick) { - overlay = React.DOM.div({ - style: { - position: 'absolute', - height: '100%', - width: '100%', - backgroundColor: 'transparent' - }, - onClick: this.click - }) - } - if (this.props.mask) { - mask = React.DOM.img({ - src: 'img/mask.svg', - style: { - position: 'absolute', - left: '50%', - top: '50%', - marginLeft: -330/2 + 'px', - marginTop: -174/2 + 'px', - } - }) - } - return React.DOM.div( - {style: {overflow: 'hidden'}}, - React.DOM.canvas({ - ref: 'canvas', - style: {position: 'absolute'}, - width: window.innerWidth, - height: window.innerHeight, - }), - mask, - overlay - ) - } - -} - diff --git a/www/app/components/wizard/add_provider_modal.js b/www/app/components/wizard/add_provider_modal.js deleted file mode 100644 index bc5e0236..00000000 --- a/www/app/components/wizard/add_provider_modal.js +++ /dev/null @@ -1,94 +0,0 @@ -// -// A modal popup to add a new provider. -// - -import React from 'react' -import { FormGroup, ControlLabel, FormControl, HelpBlock, Button, Modal } from 'react-bootstrap' -import Spinner from '../spinner' -import Validate from '../../lib/validate' -import App from '../../app' - -class AddProviderModal extends React.Component { - - static get defaultProps() {return{ - title: 'Add a provider', - onClose: null - }} - - constructor(props) { - super(props) - this.state = { - validationState: null, - errorMsg: null, - domain: "" - } - this.accept = this.accept.bind(this) - this.cancel = this.cancel.bind(this) - this.changed = this.changed.bind(this) - } - - accept() { - if (this.state.domain) { - App.providers.add(this.state.domain) - } - this.props.onClose() - } - - cancel() { - this.props.onClose() - } - - changed(e) { - let domain = e.target.value - let newState = null - let newMsg = null - - if (domain.length > 0) { - let error = Validate.domain(domain) - newState = error ? 'error' : 'success' - newMsg = error - } - this.setState({ - domain: domain, - validationState: newState, - errorMsg: newMsg - }) - } - - render() { - let help = null - if (this.state.errorMsg) { - help = {this.state.errorMsg} - } else { - help =   - } - let form =
    - - Domain - - - {help} - - -
    - - return( - - - {this.props.title} - - - {form} - - - ) - } -} - -export default AddProviderModal \ No newline at end of file diff --git a/www/app/components/wizard/index.js b/www/app/components/wizard/index.js deleted file mode 100644 index 613b88fd..00000000 --- a/www/app/components/wizard/index.js +++ /dev/null @@ -1,38 +0,0 @@ -// -// The provider setup wizard -// - -import React from 'react' -import App from 'app' - -import ProviderSelectStage from './provider_select_stage' -import './wizard.less' - -export default class Wizard extends React.Component { - - constructor(props) { - super(props) - this.state = { - stage: 'provider' - } - } - - setStage(stage) { - this.setState({stage: stage}) - } - - render() { - let stage = null - switch(this.state.stage) { - case 'provider': - stage = - break - } - return( -
    - {stage} -
    - ) - } - -} diff --git a/www/app/components/wizard/provider_select_stage.js b/www/app/components/wizard/provider_select_stage.js deleted file mode 100644 index 20674be1..00000000 --- a/www/app/components/wizard/provider_select_stage.js +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react' -import {Button, ButtonGroup, ButtonToolbar, Glyphicon} from 'react-bootstrap' - -import App from 'app' -import ListEdit from 'components/list_edit' -import StageLayout from './stage_layout' -import AddProviderModal from './add_provider_modal' - -export default class ProviderSelectStage extends React.Component { - - static get defaultProps() {return{ - title: "Choose a provider", - subtitle: "This doesn't work yet" - }} - - constructor(props) { - super(props) - let domains = this.currentDomains() - this.state = { - domains: domains, - showModal: false - } - this.add = this.add.bind(this) - this.remove = this.remove.bind(this) - this.close = this.close.bind(this) - this.previous = this.previous.bind(this) - } - - currentDomains() { - // return(App.providers.domains().slice() || []) - return ['domain1', 'domain2', 'domain3'] - } - - add() { - this.setState({showModal: true}) - } - - remove(provider) { - // App.providers.remove(provider) - this.setState({domains: this.currentDomains()}) - } - - close() { - let domains = this.currentDomains() - if (domains.length != this.state.domains.length) { - // this is ugly, but i could not get selection working - // by passing it as a property - this.refs.list.setSelected(0) - } - this.setState({ - domains: domains, - showModal: false - }) - } - - previous() { - App.start() - } - - render() { - let modal = null - if (this.state.showModal) { - modal = - } - let buttons = ( - - - - - ) - let select = - return( - - {select} - {modal} - - ) - } -} diff --git a/www/app/components/wizard/stage_layout.js b/www/app/components/wizard/stage_layout.js deleted file mode 100644 index 31540221..00000000 --- a/www/app/components/wizard/stage_layout.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react' - -class StageLayout extends React.Component { - - static get defaultProps() {return{ - title: 'untitled', - subtitle: null, - buttons: null - }} - - constructor(props) { - super(props) - } - - render() { - let subtitle = null - if (this.props.subtitle) { - subtitle = {this.props.subtitle} - } - return( -
    -
    - {this.props.title} - {subtitle} -
    -
    - {this.props.children} -
    -
    - {this.props.buttons} -
    -
    - ) - } -} - -export default StageLayout \ No newline at end of file diff --git a/www/app/components/wizard/wizard.less b/www/app/components/wizard/wizard.less deleted file mode 100644 index 29efc20e..00000000 --- a/www/app/components/wizard/wizard.less +++ /dev/null @@ -1,44 +0,0 @@ -.wizard .stage { - position: absolute; - height: 100%; - width: 100%; - - display: -webkit-flex; - display: flex; - -webkit-flex-direction: column; - flex-direction: column; - -webkit-flex: 1; - flex: 1; -} - -.wizard .stage .footer { - -webkit-flex: 0 0 auto; - flex: 0 0 auto; - background-color: #ddd; - padding: 20px; - text-align: right; -} - -.wizard .stage .header { - -webkit-flex: 0 0 auto; - flex: 0 0 auto; - padding: 20px; - background-color: #333; - color: white; - font-size: 2em; - span { - margin-left: 10px; - font-size: 0.5em; - } -} - -.wizard .stage .body { - -webkit-flex: 1 1 auto; - flex: 1 1 auto; - padding: 20px; - overflow: auto; - display: -webkit-flex; - display: flex; - -webkit-flex-direction: column; - flex-direction: column; -} \ No newline at end of file diff --git a/www/app/css/bootstrap.less b/www/app/css/bootstrap.less deleted file mode 100644 index 3b772284..00000000 --- a/www/app/css/bootstrap.less +++ /dev/null @@ -1,5 +0,0 @@ -// -// require npm modules 'bootstrap' -// -@import "~bootstrap/less/bootstrap"; - diff --git a/www/app/css/common.css b/www/app/css/common.css deleted file mode 100644 index acf164ee..00000000 --- a/www/app/css/common.css +++ /dev/null @@ -1,80 +0,0 @@ -body { - padding: 0; - margin: 0; -} - -.debug-panel { - background-color: rgba(0,0,0,0.1); - position: absolute; - bottom: 0; - left: 50%; - z-index: 1000; - padding: 20px; -} -.debug-panel a { - cursor: pointer; - margin: 10px; -} - - -.area-light { - background-color: #f7f7f7; -} -.area-dark { - background-color: #e7e7e7; -} -.area-clear { - background-color: #fff; -} - -/* - * Greeter - */ -.greeter { - border-color: #555; - border-width: 8px; -} - -/* - * bootstrap - */ - -.help-block { - margin: 2px 0 0 0; - font-size: small; - opacity: 0.7; -} -.btn.btn-inverse { - color: white; - background-color: #333; -} -.btn.btn-flat { - border-color: transparent; - background-color: transparent; -} - -/*.btn.btn-default { - background-color: #eee !important; -} -*/ - -/* - * center component - */ - -.center-container { - position: absolute; - display: -webkit-flex; - -webkit-flex-flow: row nowrap; - -webkit-justify-content: center; - -webkit-align-content: center; - -webkit-align-items: center; - top: 0px; - left: 0px; - height: 100%; - width: 100%; -} - -.center-container .center-item { - -webkit-flex: 0 1 auto; -} diff --git a/www/app/img/arrow-down.svg b/www/app/img/arrow-down.svg deleted file mode 100644 index 14c4b5d1..00000000 --- a/www/app/img/arrow-down.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/www/app/img/arrow-up.svg b/www/app/img/arrow-up.svg deleted file mode 100644 index 30ea8fde..00000000 --- a/www/app/img/arrow-up.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/www/app/img/cloud.svg b/www/app/img/cloud.svg deleted file mode 100644 index bf7a94af..00000000 --- a/www/app/img/cloud.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/www/app/img/disabled.svg b/www/app/img/disabled.svg deleted file mode 100644 index 90804d83..00000000 --- a/www/app/img/disabled.svg +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - diff --git a/www/app/img/envelope.svg b/www/app/img/envelope.svg deleted file mode 100644 index 5775e097..00000000 --- a/www/app/img/envelope.svg +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - diff --git a/www/app/img/gear.svg b/www/app/img/gear.svg deleted file mode 100644 index be5a5b33..00000000 --- a/www/app/img/gear.svg +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - diff --git a/www/app/img/mask.svg b/www/app/img/mask.svg deleted file mode 100644 index f254c5e0..00000000 --- a/www/app/img/mask.svg +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - diff --git a/www/app/img/off.svg b/www/app/img/off.svg deleted file mode 100644 index 35c49a56..00000000 --- a/www/app/img/off.svg +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - diff --git a/www/app/img/on.svg b/www/app/img/on.svg deleted file mode 100644 index 36938e0a..00000000 --- a/www/app/img/on.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/www/app/img/planet.svg b/www/app/img/planet.svg deleted file mode 100644 index 5697d5d6..00000000 --- a/www/app/img/planet.svg +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - diff --git a/www/app/img/unknown.svg b/www/app/img/unknown.svg deleted file mode 100644 index 60038638..00000000 --- a/www/app/img/unknown.svg +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - diff --git a/www/app/img/user.svg b/www/app/img/user.svg deleted file mode 100644 index ad86a757..00000000 --- a/www/app/img/user.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/www/app/img/wait.svg b/www/app/img/wait.svg deleted file mode 100644 index 2ae0113a..00000000 --- a/www/app/img/wait.svg +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - diff --git a/www/app/index.html b/www/app/index.html deleted file mode 100644 index 440e0b6f..00000000 --- a/www/app/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Bitmask - - - - -
    - - - \ No newline at end of file diff --git a/www/app/lib/bitmask.js b/www/app/lib/bitmask.js deleted file mode 100644 index fedd5fcd..00000000 --- a/www/app/lib/bitmask.js +++ /dev/null @@ -1,306 +0,0 @@ -// bitmask.js -// Copyright (C) 2016 LEAP -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -/** - * bitmask object - * - * Contains all the bitmask API mapped by sections - * - user. User management like login, creation, ... - * - mail. Email service control. - * - keys. Keyring operations. - * - events. For registering to events. - * - * Every function returns a Promise that will be triggered once the request is - * finished or will fail if there was any error. Errors are always user readable - * strings. - */ - -try { - // Use Promises in non-ES6 compliant engines. - eval('import "babel-polyfill";') -} -catch (err) {} - -var bitmask = function(){ - var event_handlers = {}; - - var api_url = '/API/'; - if (window.location.protocol === "file:") { - api_url = 'http://localhost:7070/API/'; - } - - function call(command) { - var url = api_url + command.slice(0, 2).join('/'); - var data = JSON.stringify(command.slice(2)); - - return new Promise(function(resolve, reject) { - var req = new XMLHttpRequest(); - req.open('POST', url); - - req.onload = function() { - if (req.status == 200) { - parseResponse(req.response, resolve, reject); - } - else { - reject(Error(req.statusText)); - } - }; - - req.onerror = function() { - reject(Error("Network Error")); - }; - - req.send(data); - }); - }; - - function parseResponse(raw_response, resolve, reject) { - var response = JSON.parse(raw_response); - if (response.error === null) { - resolve(response.result); - } else { - reject(response.error); - } - }; - - function event_polling() { - call(['events', 'poll']).then(function(response) { - if (response !== null) { - evnt = response[0]; - content = response[1]; - if (evnt in event_handlers) { - event_handlers[evnt](evnt, content); - } - } - event_polling(); - }, function(error) { - setTimeout(event_polling, 5000); - }); - }; - event_polling(); - - function private_str(priv) { - if (priv) { - return 'private' - } - return 'public' - }; - - return { - bonafide: { - provider: { - create: function(domain) { - return call(['bonafide', 'provider', 'create', domain]); - }, - - read: function(domain) { - return call(['bonafide', 'provider', 'read', domain]); - }, - - delete: function(domain) { - return call(['bonafide', 'provider', 'delete', domain]); - }, - - list: function(seeded) { - if (typeof seeded !== 'boolean') { - seeded = false; - } - return call(['bonafide', 'provider', 'list', seeded]); - } - }, - - /** - * uids are of the form user@provider.net - */ - user: { - /** - * Check wich user is active - * - * @return {Promise} The uid of the active user - */ - active: function() { - return call(['bonafide', 'user', 'active']); - }, - - /** - * Register a new user - * - * @param {string} uid The uid to be created - * @param {string} password The user password - * @param {boolean} autoconf If the provider should be autoconfigured if it's not allready known - * If it's not provided it will default to false - */ - create: function(uid, password, autoconf) { - if (typeof autoconf !== 'boolean') { - autoconf = false; - } - return call(['bonafide', 'user', 'create', uid, password, autoconf]); - }, - - /** - * Login - * - * @param {string} uid The uid to log in - * @param {string} password The user password - * @param {boolean} autoconf If the provider should be autoconfigured if it's not allready known - * If it's not provided it will default to false - */ - auth: function(uid, password, autoconf) { - if (typeof autoconf !== 'boolean') { - autoconf = false; - } - return call(['bonafide', 'user', 'authenticate', uid, password, autoconf]); - }, - - /** - * Logout - * - * @param {string} uid The uid to log out. - * If no uid is provided the active user will be used - */ - logout: function(uid) { - if (typeof uid !== 'string') { - uid = ""; - } - return call(['bonafide', 'user', 'logout', uid]); - } - } - }, - - mail: { - /** - * Check the status of the email service - * - * @return {Promise} User readable status - */ - status: function() { - return call(['mail', 'status']); - }, - - /** - * Get the token of the active user. - * - * This token is used as password to authenticate in the IMAP and SMTP services. - * - * @return {Promise} The token - */ - get_token: function() { - return call(['mail', 'get_token']); - } - }, - - /** - * A KeyObject have the following attributes: - * - address {string} the email address for wich this key is active - * - fingerprint {string} the fingerprint of the key - * - length {number} the size of the key bits - * - private {bool} if the key is private - * - uids {[string]} the uids in the key - * - key_data {string} the key content - * - validation {string} the validation level which this key was found - * - expiry_date {string} date when the key expires - * - refreshed_at {string} date of the last refresh of the key - * - audited_at {string} date of the last audit (unused for now) - * - sign_used {bool} if has being used to checking signatures - * - enc_used {bool} if has being used to encrypt - */ - keys: { - /** - * List all the keys in the keyring - * - * @param {boolean} priv Should list private keys? - * If it's not provided the public ones will be listed. - * - * @return {Promise<[KeyObject]>} List of keys in the keyring - */ - list: function(priv) { - return call(['keys', 'list', private_str(priv)]); - }, - - /** - * Export key - * - * @param {string} address The email address of the key - * @param {boolean} priv Should get the private key? - * If it's not provided the public one will be fetched. - * - * @return {Promise} The key - */ - exprt: function(address, priv) { - return call(['keys', 'export', address, private_str(priv)]); - }, - - /** - * Insert key - * - * @param {string} address The email address of the key - * @param {string} rawkey The key material - * @param {string} validation The validation level of the key - * If it's not provided 'Fingerprint' level will be used. - * - * @return {Promise} The key - */ - insert: function(address, rawkey, validation) { - if (typeof validation !== 'string') { - validation = 'Fingerprint'; - } - return call(['keys', 'insert', address, validation, rawkey]); - }, - - /** - * Delete a key - * - * @param {string} address The email address of the key - * @param {boolean} priv Should get the private key? - * If it's not provided the public one will be deleted. - * - * @return {Promise} The key - */ - del: function(address, priv) { - return call(['keys', 'delete', address, private_str(priv)]); - } - }, - - events: { - /** - * Register func for an event - * - * @param {string} evnt The event to register - * @param {function} func The function that will be called on each event. - * It has to be like: function(event, content) {} - * Where content will be a list of strings. - */ - register: function(evnt, func) { - event_handlers[evnt] = func; - return call(['events', 'register', evnt]) - }, - - /** - * Unregister from an event - * - * @param {string} evnt The event to unregister - */ - unregister: function(evnt) { - delete event_handlers[evnt]; - return call(['events', 'unregister', evnt]) - } - } - }; -}(); - -try { - module.exports = bitmask -} catch(err) {} diff --git a/www/app/lib/color.js b/www/app/lib/color.js deleted file mode 100644 index 5b1dfee9..00000000 --- a/www/app/lib/color.js +++ /dev/null @@ -1,65 +0,0 @@ -// -// Color.hsv().css -// -// RGB values are 0..255 -// HSV values are 0..1 -// - -function compose(value) - return [ - Math.round(value * 255), - Math.round(value * 255), - Math.round(value * 255) - } -} - -class Color { - - constructor(r, g, b, a) { - this.r = r - this.g = g - this.b = b - this.a = a - } - - // - // alternate hsv factory - // - static hsv(h,s,v) { - let out = null - h = h % 360; - s = Math.max(0, Math.min(1, s)) - v = Math.max(0, Math.min(1, v)) - - if (s == 0) { - let grey = Math.ceil(v*255) - out = [grey, grey, grey] - } - - let b = ((1 - s) * v); - let vb = v - b; - let hm = h % 60; - switch((h/60)|0) { - case 0: - out = compose(v, vb * h / 60 + b, b); break - case 1: - out = compose(vb * (60 - hm) / 60 + b, v, b); break - case 2: - out = compose(b, v, vb * hm / 60 + b); break - case 3: - out = compose(b, vb * (60 - hm) / 60 + b, v); break - case 4: - out = compose(vb * hm / 60 + b, b, v); break - case 5: - out = compose(v, b, vb * (60 - hm) / 60 + b); break - } - - return new Color(...out) - } - - css() { - return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})` - } -} - -export default Color \ No newline at end of file diff --git a/www/app/lib/colors.js b/www/app/lib/colors.js deleted file mode 100644 index d86ca0a8..00000000 --- a/www/app/lib/colors.js +++ /dev/null @@ -1,289 +0,0 @@ -// -// a palette from google's material design -// - -export const red50 = '#ffebee'; -export const red100 = '#ffcdd2'; -export const red200 = '#ef9a9a'; -export const red300 = '#e57373'; -export const red400 = '#ef5350'; -export const red500 = '#f44336'; -export const red600 = '#e53935'; -export const red700 = '#d32f2f'; -export const red800 = '#c62828'; -export const red900 = '#b71c1c'; -export const redA100 = '#ff8a80'; -export const redA200 = '#ff5252'; -export const redA400 = '#ff1744'; -export const redA700 = '#d50000'; - -export const pink50 = '#fce4ec'; -export const pink100 = '#f8bbd0'; -export const pink200 = '#f48fb1'; -export const pink300 = '#f06292'; -export const pink400 = '#ec407a'; -export const pink500 = '#e91e63'; -export const pink600 = '#d81b60'; -export const pink700 = '#c2185b'; -export const pink800 = '#ad1457'; -export const pink900 = '#880e4f'; -export const pinkA100 = '#ff80ab'; -export const pinkA200 = '#ff4081'; -export const pinkA400 = '#f50057'; -export const pinkA700 = '#c51162'; - -export const purple50 = '#f3e5f5'; -export const purple100 = '#e1bee7'; -export const purple200 = '#ce93d8'; -export const purple300 = '#ba68c8'; -export const purple400 = '#ab47bc'; -export const purple500 = '#9c27b0'; -export const purple600 = '#8e24aa'; -export const purple700 = '#7b1fa2'; -export const purple800 = '#6a1b9a'; -export const purple900 = '#4a148c'; -export const purpleA100 = '#ea80fc'; -export const purpleA200 = '#e040fb'; -export const purpleA400 = '#d500f9'; -export const purpleA700 = '#aa00ff'; - -export const deepPurple50 = '#ede7f6'; -export const deepPurple100 = '#d1c4e9'; -export const deepPurple200 = '#b39ddb'; -export const deepPurple300 = '#9575cd'; -export const deepPurple400 = '#7e57c2'; -export const deepPurple500 = '#673ab7'; -export const deepPurple600 = '#5e35b1'; -export const deepPurple700 = '#512da8'; -export const deepPurple800 = '#4527a0'; -export const deepPurple900 = '#311b92'; -export const deepPurpleA100 = '#b388ff'; -export const deepPurpleA200 = '#7c4dff'; -export const deepPurpleA400 = '#651fff'; -export const deepPurpleA700 = '#6200ea'; - -export const indigo50 = '#e8eaf6'; -export const indigo100 = '#c5cae9'; -export const indigo200 = '#9fa8da'; -export const indigo300 = '#7986cb'; -export const indigo400 = '#5c6bc0'; -export const indigo500 = '#3f51b5'; -export const indigo600 = '#3949ab'; -export const indigo700 = '#303f9f'; -export const indigo800 = '#283593'; -export const indigo900 = '#1a237e'; -export const indigoA100 = '#8c9eff'; -export const indigoA200 = '#536dfe'; -export const indigoA400 = '#3d5afe'; -export const indigoA700 = '#304ffe'; - -export const blue50 = '#e3f2fd'; -export const blue100 = '#bbdefb'; -export const blue200 = '#90caf9'; -export const blue300 = '#64b5f6'; -export const blue400 = '#42a5f5'; -export const blue500 = '#2196f3'; -export const blue600 = '#1e88e5'; -export const blue700 = '#1976d2'; -export const blue800 = '#1565c0'; -export const blue900 = '#0d47a1'; -export const blueA100 = '#82b1ff'; -export const blueA200 = '#448aff'; -export const blueA400 = '#2979ff'; -export const blueA700 = '#2962ff'; - -export const lightBlue50 = '#e1f5fe'; -export const lightBlue100 = '#b3e5fc'; -export const lightBlue200 = '#81d4fa'; -export const lightBlue300 = '#4fc3f7'; -export const lightBlue400 = '#29b6f6'; -export const lightBlue500 = '#03a9f4'; -export const lightBlue600 = '#039be5'; -export const lightBlue700 = '#0288d1'; -export const lightBlue800 = '#0277bd'; -export const lightBlue900 = '#01579b'; -export const lightBlueA100 = '#80d8ff'; -export const lightBlueA200 = '#40c4ff'; -export const lightBlueA400 = '#00b0ff'; -export const lightBlueA700 = '#0091ea'; - -export const cyan50 = '#e0f7fa'; -export const cyan100 = '#b2ebf2'; -export const cyan200 = '#80deea'; -export const cyan300 = '#4dd0e1'; -export const cyan400 = '#26c6da'; -export const cyan500 = '#00bcd4'; -export const cyan600 = '#00acc1'; -export const cyan700 = '#0097a7'; -export const cyan800 = '#00838f'; -export const cyan900 = '#006064'; -export const cyanA100 = '#84ffff'; -export const cyanA200 = '#18ffff'; -export const cyanA400 = '#00e5ff'; -export const cyanA700 = '#00b8d4'; - -export const teal50 = '#e0f2f1'; -export const teal100 = '#b2dfdb'; -export const teal200 = '#80cbc4'; -export const teal300 = '#4db6ac'; -export const teal400 = '#26a69a'; -export const teal500 = '#009688'; -export const teal600 = '#00897b'; -export const teal700 = '#00796b'; -export const teal800 = '#00695c'; -export const teal900 = '#004d40'; -export const tealA100 = '#a7ffeb'; -export const tealA200 = '#64ffda'; -export const tealA400 = '#1de9b6'; -export const tealA700 = '#00bfa5'; - -export const green50 = '#e8f5e9'; -export const green100 = '#c8e6c9'; -export const green200 = '#a5d6a7'; -export const green300 = '#81c784'; -export const green400 = '#66bb6a'; -export const green500 = '#4caf50'; -export const green600 = '#43a047'; -export const green700 = '#388e3c'; -export const green800 = '#2e7d32'; -export const green900 = '#1b5e20'; -export const greenA100 = '#b9f6ca'; -export const greenA200 = '#69f0ae'; -export const greenA400 = '#00e676'; -export const greenA700 = '#00c853'; - -export const lightGreen50 = '#f1f8e9'; -export const lightGreen100 = '#dcedc8'; -export const lightGreen200 = '#c5e1a5'; -export const lightGreen300 = '#aed581'; -export const lightGreen400 = '#9ccc65'; -export const lightGreen500 = '#8bc34a'; -export const lightGreen600 = '#7cb342'; -export const lightGreen700 = '#689f38'; -export const lightGreen800 = '#558b2f'; -export const lightGreen900 = '#33691e'; -export const lightGreenA100 = '#ccff90'; -export const lightGreenA200 = '#b2ff59'; -export const lightGreenA400 = '#76ff03'; -export const lightGreenA700 = '#64dd17'; - -export const lime50 = '#f9fbe7'; -export const lime100 = '#f0f4c3'; -export const lime200 = '#e6ee9c'; -export const lime300 = '#dce775'; -export const lime400 = '#d4e157'; -export const lime500 = '#cddc39'; -export const lime600 = '#c0ca33'; -export const lime700 = '#afb42b'; -export const lime800 = '#9e9d24'; -export const lime900 = '#827717'; -export const limeA100 = '#f4ff81'; -export const limeA200 = '#eeff41'; -export const limeA400 = '#c6ff00'; -export const limeA700 = '#aeea00'; - -export const yellow50 = '#fffde7'; -export const yellow100 = '#fff9c4'; -export const yellow200 = '#fff59d'; -export const yellow300 = '#fff176'; -export const yellow400 = '#ffee58'; -export const yellow500 = '#ffeb3b'; -export const yellow600 = '#fdd835'; -export const yellow700 = '#fbc02d'; -export const yellow800 = '#f9a825'; -export const yellow900 = '#f57f17'; -export const yellowA100 = '#ffff8d'; -export const yellowA200 = '#ffff00'; -export const yellowA400 = '#ffea00'; -export const yellowA700 = '#ffd600'; - -export const amber50 = '#fff8e1'; -export const amber100 = '#ffecb3'; -export const amber200 = '#ffe082'; -export const amber300 = '#ffd54f'; -export const amber400 = '#ffca28'; -export const amber500 = '#ffc107'; -export const amber600 = '#ffb300'; -export const amber700 = '#ffa000'; -export const amber800 = '#ff8f00'; -export const amber900 = '#ff6f00'; -export const amberA100 = '#ffe57f'; -export const amberA200 = '#ffd740'; -export const amberA400 = '#ffc400'; -export const amberA700 = '#ffab00'; - -export const orange50 = '#fff3e0'; -export const orange100 = '#ffe0b2'; -export const orange200 = '#ffcc80'; -export const orange300 = '#ffb74d'; -export const orange400 = '#ffa726'; -export const orange500 = '#ff9800'; -export const orange600 = '#fb8c00'; -export const orange700 = '#f57c00'; -export const orange800 = '#ef6c00'; -export const orange900 = '#e65100'; -export const orangeA100 = '#ffd180'; -export const orangeA200 = '#ffab40'; -export const orangeA400 = '#ff9100'; -export const orangeA700 = '#ff6d00'; - -export const deepOrange50 = '#fbe9e7'; -export const deepOrange100 = '#ffccbc'; -export const deepOrange200 = '#ffab91'; -export const deepOrange300 = '#ff8a65'; -export const deepOrange400 = '#ff7043'; -export const deepOrange500 = '#ff5722'; -export const deepOrange600 = '#f4511e'; -export const deepOrange700 = '#e64a19'; -export const deepOrange800 = '#d84315'; -export const deepOrange900 = '#bf360c'; -export const deepOrangeA100 = '#ff9e80'; -export const deepOrangeA200 = '#ff6e40'; -export const deepOrangeA400 = '#ff3d00'; -export const deepOrangeA700 = '#dd2c00'; - -export const brown50 = '#efebe9'; -export const brown100 = '#d7ccc8'; -export const brown200 = '#bcaaa4'; -export const brown300 = '#a1887f'; -export const brown400 = '#8d6e63'; -export const brown500 = '#795548'; -export const brown600 = '#6d4c41'; -export const brown700 = '#5d4037'; -export const brown800 = '#4e342e'; -export const brown900 = '#3e2723'; - -export const blueGrey50 = '#eceff1'; -export const blueGrey100 = '#cfd8dc'; -export const blueGrey200 = '#b0bec5'; -export const blueGrey300 = '#90a4ae'; -export const blueGrey400 = '#78909c'; -export const blueGrey500 = '#607d8b'; -export const blueGrey600 = '#546e7a'; -export const blueGrey700 = '#455a64'; -export const blueGrey800 = '#37474f'; -export const blueGrey900 = '#263238'; - -export const grey50 = '#fafafa'; -export const grey100 = '#f5f5f5'; -export const grey200 = '#eeeeee'; -export const grey300 = '#e0e0e0'; -export const grey400 = '#bdbdbd'; -export const grey500 = '#9e9e9e'; -export const grey600 = '#757575'; -export const grey700 = '#616161'; -export const grey800 = '#424242'; -export const grey900 = '#212121'; - -export const black = '#000000'; -export const white = '#ffffff'; - -export const transparent = 'rgba(0, 0, 0, 0)'; -export const fullBlack = 'rgba(0, 0, 0, 1)'; -export const darkBlack = 'rgba(0, 0, 0, 0.87)'; -export const lightBlack = 'rgba(0, 0, 0, 0.54)'; -export const minBlack = 'rgba(0, 0, 0, 0.26)'; -export const faintBlack = 'rgba(0, 0, 0, 0.12)'; -export const fullWhite = 'rgba(255, 255, 255, 1)'; -export const darkWhite = 'rgba(255, 255, 255, 0.87)'; -export const lightWhite = 'rgba(255, 255, 255, 0.54)'; \ No newline at end of file diff --git a/www/app/lib/common.js b/www/app/lib/common.js deleted file mode 100644 index 14a30c32..00000000 --- a/www/app/lib/common.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * pollute the global namespace with some useful utils - */ - -import React from 'react' - -window.elem = React.createElement \ No newline at end of file diff --git a/www/app/lib/validate.js b/www/app/lib/validate.js deleted file mode 100644 index 4e672e8f..00000000 --- a/www/app/lib/validate.js +++ /dev/null @@ -1,82 +0,0 @@ -// https://github.com/dropbox/zxcvbn - -var DOMAIN_RE = /^((?:(?:(?:\w[\.\-\+]?)*)\w)+)((?:(?:(?:\w[\.\-\+]?){0,62})\w)+)\.(\w{2,6})$/ -var USER_RE = /^[a-z0-9_-]{1,200}$/ -var USER_INT_RE = /^[\.@a-z0-9_-]*$/ - -// -// Validations returns an error message or false if no errors -// - -class Validations { - - domain(input) { - if (!input.match(DOMAIN_RE)) { - return "Not a domain name" - } else { - return null - } - } - - usernameInteractive(input) { - if (!input.match(USER_INT_RE)) { - return "Username contains an invalid character" - } - return false - } - - username(input) { - if (!input.match(USER_INT_RE)) { - return "Username contains an invalid character" - } - if (!input.match('@')) { - return "Username must be in the form username@domain" - } - let parts = input.split('@') - let userpart = parts[0] - let domainpart = parts[1] - if (!userpart.match(USER_RE)) { - return "Username contains an invalid character" - } else if (!domainpart.match(DOMAIN_RE)) { - return "Username must include a valid domain name." - } - return false - } - - passwordStrength(passwd) { - if (typeof(zxcvbn) == 'function') { - // zxcvbn performs very slow on long strings, so we cap - // the calculation at 30 characters - return zxcvbn(passwd.substring(0,30)) - } else { - return null - } - } - - // - // loads the zxcvbn library. because this library is big, we don't load it - // every time, just when needed. - // - // this is the webpack way to do this: - // - // require.ensure([], function () { - // var zxcvbn = require('zxcvbn'); - // }); - // - // that works, but requires that we also process the original coffeescript - // source if we want to avoid warning messages. - // - loadPasswdLib(onload) { - var id = "zxcvbn-script" - if (!document.getElementById(id)) { - var script = document.createElement('script') - script.id = id - script.onload = onload - script.src = './js/zxcvbn.js' - document.getElementsByTagName('script')[0].appendChild(script) - } - } -} - -var Validate = new Validations() -export default Validate diff --git a/www/app/main.js b/www/app/main.js deleted file mode 100644 index b1628953..00000000 --- a/www/app/main.js +++ /dev/null @@ -1,26 +0,0 @@ -// -// main entry point for app execution -// -// This is determined by the 'entry' option in webpack.config.js -// - -import React from 'react' -import ReactDOM from 'react-dom' - -import PanelSwitcher from 'components/panel_switcher' -import App from 'app' - -class Main extends React.Component { - render() { - return React.createElement(PanelSwitcher) - } - - componentDidMount() { - App.start() - } -} - -ReactDOM.render( - React.createElement(Main), - document.getElementById('app') -) diff --git a/www/app/models/account.js b/www/app/models/account.js deleted file mode 100644 index 52fea93d..00000000 --- a/www/app/models/account.js +++ /dev/null @@ -1,143 +0,0 @@ -// -// An account is an abstraction of a user and a provider. -// The user part is optional, so an Account might just represent a provider. -// - -import bitmask from 'lib/bitmask' - -export default class Account { - - constructor(address, props={}) { - this.address = address - this._authenticated = props.authenticated - } - - // - // currently, bitmask.js uses address for id, so we return address here too. - // also, we don't know uuid until after authentication. - // - // TODO: change to uuid when possible. - // - get id() { - return this._address - } - - get domain() { - return this._address.split('@')[1] - } - - get address() { - return this._address - } - - set address(address) { - if (!address.match('@')) { - this._address = '@' + address - } else { - this._address = address - } - } - - get userpart() { - return this._address.split('@')[0] - } - - get authenticated() { - return this._authenticated - } - - get hasEmail() { - return true - } - - // - // returns a promise, fulfill is passed account object - // - login(password) { - return bitmask.bonafide.user.auth(this.address, password).then( - response => { - if (response.uuid) { - this._uuid = response.uuid - this._authenticated = true - } - return this - } - ) - } - - // - // returns a promise, fulfill is passed account object - // - logout() { - return bitmask.bonafide.user.logout(this.id).then( - response => { - this._authenticated = false - this._address = '@' + this.domain - return this - } - ) - } - - // - // returns the matching account in the list of accounts, or adds it - // if it is not already present. - // - static find(address) { - // search by full address - let account = Account.list.find(i => { - return i.address == address - }) - // failing that, search by domain - if (!account) { - let domain = '@' + address.split('@')[1] - account = Account.list.find(i => { - return i.address == domain - }) - if (account) { - account.address = address - } - } - // failing that, create new account - if (!account) { - account = new Account(address) - Account.list.push(account) - } - return account - } - - // - // returns a promise, fullfill is passed account object - // - static active() { - return bitmask.bonafide.user.active().then( - response => { - if (response.user == '') { - return null - } else { - return new Account(response.user, {authenticated: true}) - } - } - ) - } - - static add(account) { - if (!Account.list.find(i => {return i.id == account.id})) { - Account.list.push(account) - } - } - - static remove(account) { - Account.list = Account.list.filter(i => { - return i.id != account.id - }) - } - // return Account.list - // return new Promise(function(resolve, reject) { - // window.setTimeout(function() { - // resolve(['@blah', '@lala']) - // }, 1000) - // }) - // } -} - -Account.list = [] diff --git a/www/app/models/dummy_account.js b/www/app/models/dummy_account.js deleted file mode 100644 index 99fb6623..00000000 --- a/www/app/models/dummy_account.js +++ /dev/null @@ -1,33 +0,0 @@ -// -// A proxy of an account, but with a different ID. For testing. -// - -import bitmask from 'lib/bitmask' - -export default class DummyAccount { - - constructor(account) { - this.account = account - } - - get id() { - return 'dummy--' + this.account.address - } - - get domain() {return this.account.domain} - get address() {return this.account.address} - get userpart() {return this.account.userpart} - get authenticated() {return this.account.authenticated} - get hasEmail() {return this.account.hasEmail} - login(password) {return this.account.login(password)} - - logout() { - return bitmask.bonafide.user.logout(this.address).then( - response => { - this._authenticated = false - this._address = '@' + this.domain - return this - } - ) - } -} diff --git a/www/package.json b/www/package.json deleted file mode 100644 index d491edd8..00000000 --- a/www/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "bitmask_js", - "version": "0.0.1", - "description": "bitmask user interface in javascript", - "license": "GPL-3.0", - "homepage": "https://bitmask.net", - "repository": "https://leap.se/git/bitmask_client.git", - "dependencies": {}, - "devDependencies": { - "babel": "^6.5.2", - "babel-loader": "^6.2.4", - "babel-polyfill": "^6.13.0", - "babel-preset-es2015": "^6.9.0", - "babel-preset-react": "^6.11.1", - "babel-preset-stage-0": "^6.5.0", - "bootstrap": "^3.3.7", - "copy-webpack-plugin": "^3.0.1", - "css-loader": "^0.23.1", - "less": "^2.7.1", - "less-loader": "^2.2.3", - "react": "^15.2.1", - "react-bootstrap": "^0.30.2", - "react-dom": "^15.2.1", - "style-loader": "^0.13.1", - "webpack": "^1.13.1", - "zxcvbn": "^4.3.0" - }, - "scripts": { - "open": "gnome-open http://localhost:7070", - "watch": "NODE_ENV=development webpack --watch", - "build": "NODE_ENV=development webpack", - "build:production": "NODE_ENV=production webpack" - } -} diff --git a/www/pydist/README.md b/www/pydist/README.md deleted file mode 100644 index 6e2b11f6..00000000 --- a/www/pydist/README.md +++ /dev/null @@ -1,4 +0,0 @@ -This directory holds the python package of the javascript app, called 'bitmask_js'. - -Why it it done this way? By creating a python package, it is easier for the -javascript app to be distributed with the bitmask client. diff --git a/www/pydist/setup.py b/www/pydist/setup.py deleted file mode 100644 index b5d3ffb1..00000000 --- a/www/pydist/setup.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# setup.py -# Copyright (C) 2016 LEAP Encryption Acess Project -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Setup file for bitmask_js -""" - -from setuptools import setup -import datetime -import time - -long_description = \ -'''bitmask_js ------------------ -This package contains the already compiled javascript resources for the bitmask UI. - -If you want to develop for this UI, please checkout the bitmask-dev [0] repo and follow the instructions in the ui/README.md file. - -[0] https://github.com/leapcode/bitmask-dev''' - -now = datetime.datetime.now() -timestamp = time.strftime('%Y%m%d%H%M', now.timetuple()) - -setup( - name='bitmask_js', - version='0.1.%s' % timestamp, - description='Bitmask UI', - long_description=long_description, - author='LEAP Encrypted Access Project', - author_email='info@leap.se', - url='http://leap.se', - packages=['bitmask_js'], - package_data={ - '': ['public/*', - 'public/css/*', - 'public/fonts/*', - 'public/img/*', - 'publlic/js/*', - ] - } -) diff --git a/www/webpack.config.js b/www/webpack.config.js deleted file mode 100644 index c0f8d191..00000000 --- a/www/webpack.config.js +++ /dev/null @@ -1,84 +0,0 @@ -var path = require('path') -var webpack = require('webpack') -var CopyWebpackPlugin = require('copy-webpack-plugin'); - -var config = { - context: path.join(__dirname, 'app'), - entry: ['babel-polyfill', './main.js'], - output: { - path: path.join(__dirname, 'pydist', 'bitmask_js', 'public'), - filename: 'app.bundle.js' - }, - resolve: { - modulesDirectories: ['node_modules', './app'], - extensions: ['', '.js', '.jsx'] - }, - module: { - loaders: [ - // babel transform - { - test: /\.js$/, - loader: 'babel-loader', - exclude: /node_modules/, - query: { - presets: ['react', 'es2015'] - } - }, - { - test: /\.css$/, - loader: "style!css" - }, - { - test: /\.less$/, - loader: "style!css!less?noIeCompat" - } - ] - }, - plugins: [ - // don't bundle when there is an error: - new webpack.NoErrorsPlugin(), - - // https://webpack.github.io/docs/code-splitting.html - // new webpack.optimize.CommonChunkPlugin('common.js') - - // - // ASSETS - // - // If you make changes to the asset files, you will need to stop then rerun - // `npm run watch` for the changes to take effect. - // - // For more information: https://github.com/kevlened/copy-webpack-plugin - // - new CopyWebpackPlugin([ - { from: 'css/*.css' }, - { from: 'img/*'}, - { from: 'index.html' }, - { from: '../node_modules/bootstrap/dist/css/bootstrap.min.css', to: 'css' }, - { from: '../node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff', to: 'fonts' }, - { from: '../node_modules/zxcvbn/dist/zxcvbn.js', to: 'js' } - ]) - ], - stats: { - colors: true - }, - // source-map can be used in production or development - // but it creates a separate file. - devtool: 'source-map' -} - -/* -if (process.env.NODE_ENV == 'production') { - // see https://github.com/webpack/docs/wiki/optimization - config.plugins.push( - new webpack.optimize.UglifyJsPlugin({ - compress: { warnings: false }, - output: { comments: false } - }), - new webpack.optimize.DedupePlugin() - ) -} else { - config.devtool = 'inline-source-map'; -} -*/ - -module.exports = config -- cgit v1.2.3