summaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
authorelijah <elijah@riseup.net>2016-09-29 17:00:47 -0700
committerelijah <elijah@riseup.net>2016-09-29 17:00:47 -0700
commit10b0b4462107ecebffab4ce3eb0435d3c1b2dd24 (patch)
tree14ae6e69e56f53eacb299d59b0f6a1beda3063b8 /ui
parente7281dd47f375c1b0a72ae85505319c4d87fb524 (diff)
[feat] ui - allow users to change their passwords
Diffstat (limited to 'ui')
-rw-r--r--ui/app/components/login.js6
-rw-r--r--ui/app/components/main_panel/main_panel.less91
-rw-r--r--ui/app/components/main_panel/section_layout.js55
-rw-r--r--ui/app/components/main_panel/user_password_form.js123
-rw-r--r--ui/app/components/main_panel/user_section.js78
-rw-r--r--ui/app/components/password_field.js112
-rw-r--r--ui/app/lib/bitmask.js16
-rw-r--r--ui/app/lib/common.js4
-rw-r--r--ui/app/models/account.js2
9 files changed, 415 insertions, 72 deletions
diff --git a/ui/app/components/login.js b/ui/app/components/login.js
index 059551f..00f52d5 100644
--- a/ui/app/components/login.js
+++ b/ui/app/components/login.js
@@ -33,11 +33,11 @@ class Login extends React.Component {
usernameState: null, // username validation state
usernameError: false, // username help message
- password: null,
+ password: null, // the main password field
passwordState: null, // password validation state
passwordError: false, // password help message
- password2: null, // password confirmation
+ password2: null, // password confirmation
password2State: null, // password confirm validation state
password2Error: false, // password confirm help message
@@ -330,7 +330,7 @@ class Login extends React.Component {
maySubmit() {
let ok = (
- !this.stateLoading &&
+ !this.state.loading &&
!this.state.usernameError &&
this.state.username &&
this.state.password
diff --git a/ui/app/components/main_panel/main_panel.less b/ui/app/components/main_panel/main_panel.less
index 4e0ecb0..2e97913 100644
--- a/ui/app/components/main_panel/main_panel.less
+++ b/ui/app/components/main_panel/main_panel.less
@@ -69,6 +69,9 @@
border-top-left-radius: @accounts-corner - 1;
border-bottom-left-radius: @accounts-corner - 1;
z-index: 100;
+ &:hover {
+ background-color: #555;
+ }
}
.main-panel .accounts li span.domain {
@@ -141,31 +144,59 @@
//
// SECTIONS
//
-
+// html structure:
+//
+// .service-section
+// .expander
+// .shader
+// .icon
+// .inner
+// .header-row
+// .header
+// .buttons
+// .body-row
+// .status
+//
@icon-size: 32px;
@status-size: 24px;
-@section-padding: 10px;
+@section-padding: 15px;
// service sections layout
.main-panel .service-section {
display: -webkit-flex;
-webkit-flex-direction: row;
- > .icon {
+ .expander {
-webkit-flex: 0 0 auto;
}
- > .body {
+ .shade {
-webkit-flex: 1 1 auto;
- }
- > .buttons {
- -webkit-flex: 0 0 auto;
- }
- > .status {
- -webkit-flex: 0 0 auto;
display: -webkit-flex;
- -webkit-align-items: center;
- }
+ -webkit-flex-direction: row;
+ .icon {
+ -webkit-flex: 0 0 auto;
+ }
+ .status {
+ -webkit-flex: 0 0 auto;
+ }
+ .inner {
+ -webkit-flex: 1 1 auto;
+ .header-row {
+ display: -webkit-flex;
+ -webkit-flex-direction: row;
+ .header {
+ -webkit-flex: 1 1 auto;
+ }
+ .buttons {
+ -webkit-flex: 0 0 auto;
+ }
+ }
+ .body-row {
+ }
+ }
+
+ }
}
.main-panel .service-section div {
@@ -175,21 +206,35 @@
// 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
+ .shade {
+ background: #eee;
+ border-top-right-radius: 6px;
+ border-bottom-right-radius: 6px;
+ padding: @section-padding;
}
- > .icon {
+ &.wide-margin .shader {
+ padding: 18px 20px 20px 10px; // arbitrary, looks nice
+ }
+ .expander {
+ padding: 22px 6px 0px 6px;
+ width: 12px + 6px + 6px;
+ background: #e3e3e3;
+ border-top-left-radius: 6px;
+ border-bottom-left-radius: 6px;
+ &:hover.clickable {
+ background: #cfcfcf;
+ cursor: pointer;
+ }
+ }
+ .icon {
padding-right: @section-padding;
img {
width: @icon-size;
height: @icon-size;
}
}
- > .body {
+ .header {
h1 {
margin: 0;
padding: 0;
@@ -197,10 +242,11 @@
line-height: @icon-size;
}
}
- > .buttons {
+ .buttons {
padding-left: 10px;
}
- > .status {
+ .status {
+ padding-top: 5px; // maybe there is a better way to do this
padding-left: @section-padding;
width: @section-padding + @status-size;
img {
@@ -208,5 +254,8 @@
height: @status-size;
}
}
+ .body-row {
+ padding-top: @section-padding;
+ }
}
diff --git a/ui/app/components/main_panel/section_layout.js b/ui/app/components/main_panel/section_layout.js
index e7c6f2a..10c1bc1 100644
--- a/ui/app/components/main_panel/section_layout.js
+++ b/ui/app/components/main_panel/section_layout.js
@@ -4,13 +4,18 @@
//
import React from 'react'
+import { Button, Glyphicon } from 'react-bootstrap'
export default class SectionLayout extends React.Component {
static get defaultProps() {return{
- icon: null,
- buttons: null,
- status: null,
+ icon: null, // icon name
+ buttons: null, // button content
+ status: null, // must be one of: on, off, unknown, wait, disabled
+ header: null, // the first line content
+ body: null, // expanded content
+ message: null, // alert content
+ onExpand: null, // callback
className: "",
style: {}
}}
@@ -24,7 +29,21 @@ export default class SectionLayout extends React.Component {
let status = null
let icon = null
let buttons = null
+ let expander = null
+ let body = null
+ if (this.props.onExpand) {
+ let glyph = this.props.body ? 'triangle-top' : 'triangle-bottom'
+ expander = (
+ <div className="expander clickable" onClick={this.props.onExpand}>
+ <Glyphicon glyph={glyph} />
+ </div>
+ )
+ } else {
+ expander = (
+ <div className="expander" />
+ )
+ }
if (this.props.status) {
status = (
<div className="status">
@@ -39,20 +58,38 @@ export default class SectionLayout extends React.Component {
</div>
)
}
- if (this.props.buttons)
+ if (this.props.buttons) {
buttons = (
<div className="buttons">
{this.props.buttons}
</div>
)
+ }
+ if (this.props.body || this.props.message) {
+ body = (
+ <div className="body-row">
+ {this.props.message}
+ {this.props.body}
+ </div>
+ )
+ }
+
return(
<div className={className} style={this.props.style}>
- {icon}
- <div className="body">
- {this.props.children}
+ {expander}
+ <div className="shade">
+ {icon}
+ <div className="inner">
+ <div className="header-row">
+ <div className="header">
+ {this.props.header}
+ </div>
+ {buttons}
+ </div>
+ {body}
+ </div>
+ {status}
</div>
- {buttons}
- {status}
</div>
)
}
diff --git a/ui/app/components/main_panel/user_password_form.js b/ui/app/components/main_panel/user_password_form.js
new file mode 100644
index 0000000..5e26b19
--- /dev/null
+++ b/ui/app/components/main_panel/user_password_form.js
@@ -0,0 +1,123 @@
+//
+// A form to change the user password
+//
+
+import React from 'react'
+import { Button, Glyphicon, Alert } from 'react-bootstrap'
+import Spinner from 'components/spinner'
+import PasswordField from 'components/password_field'
+import Account from 'models/account'
+import bitmask from 'lib/bitmask'
+
+export default class UserPasswordForm extends React.Component {
+
+ static get defaultProps() {return{
+ account: null,
+ }}
+
+ constructor(props) {
+ super(props)
+ this.state = {
+ error: null,
+ message: null,
+ loading: false,
+ currentPassword: null,
+ newPassword: null,
+ repeatPassword: null
+ }
+ this.submit = this.submit.bind(this)
+ this.setNew = this.setNew.bind(this)
+ this.setCurrent = this.setCurrent.bind(this)
+ this.setRepeat = this.setRepeat.bind(this)
+ }
+
+ setCurrent(value) {
+ this.setState({currentPassword: value})
+ }
+
+ setNew(value) {
+ this.setState({newPassword: value})
+ }
+
+ setRepeat(value) {
+ this.setState({repeatPassword: value})
+ }
+
+ submit(e) {
+ e.preventDefault() // don't reload the page please!
+ if (!this.maySubmit()) { return }
+ this.setState({loading: true})
+ bitmask.bonafide.user.update(
+ this.props.account.address,
+ this.state.currentPassword,
+ this.state.newPassword).then(
+ response => {
+ this.setState({
+ currentPassword: null,
+ newPassword: null,
+ repeatPassword: null,
+ message: response,
+ error: null,
+ loading: false
+ })
+ }, error => {
+ this.setState({
+ error: error,
+ message: null,
+ loading: false
+ })
+ }
+ )
+ }
+
+ maySubmit() {
+ return (
+ !this.state.loading &&
+ this.state.currentPassword &&
+ this.state.newPassword &&
+ this.state.newPassword == this.state.repeatPassword
+ )
+ }
+
+ render () {
+ let submitButton = null
+ let message = null
+
+ // style may be: success, warning, danger, info
+ if (this.state.error) {
+ message = (
+ <Alert bsStyle="danger">{this.state.error}</Alert>
+ )
+ } else if (this.state.message) {
+ message = (
+ <Alert bsStyle="success">{this.state.message}</Alert>
+ )
+ }
+
+ if (this.state.loading) {
+ submitButton = <Button block disabled={true}><Spinner /></Button>
+ } else {
+ submitButton = <Button block disabled={!this.maySubmit()} onClick={this.submit}>Change</Button>
+ }
+ return (
+ <form onSubmit={this.submit}>
+ {message}
+ <PasswordField id={this.props.account.id + "-current-password"}
+ label="Current Password"
+ validationMode="none"
+ onChange={this.setCurrent} />
+ <PasswordField id={this.props.account.id + "-new-password"}
+ label="New Password"
+ validationMode="crack"
+ onChange={this.setNew} />
+ <PasswordField id={this.props.account.id + "-repeat-password"}
+ label="Repeat Password"
+ validationMode="match"
+ matchText={this.state.newPassword}
+ onChange={this.setRepeat} />
+ {submitButton}
+ </form>
+ )
+ }
+
+}
diff --git a/ui/app/components/main_panel/user_section.js b/ui/app/components/main_panel/user_section.js
index acaea23..317f993 100644
--- a/ui/app/components/main_panel/user_section.js
+++ b/ui/app/components/main_panel/user_section.js
@@ -1,6 +1,8 @@
import React from 'react'
import { Button, Glyphicon, Alert } from 'react-bootstrap'
import SectionLayout from './section_layout'
+import UserPasswordForm from './user_password_form'
+
import Login from 'components/login'
import Spinner from 'components/spinner'
import Account from 'models/account'
@@ -19,9 +21,11 @@ export default class UserSection extends React.Component {
super(props)
this.state = {
error: null,
- loading: false
+ loading: false,
+ expanded: false,
}
this.logout = this.logout.bind(this)
+ this.expand = this.expand.bind(this)
}
logout() {
@@ -38,42 +42,60 @@ export default class UserSection extends React.Component {
)
}
+ expand() {
+ this.setState({expanded: !this.state.expanded})
+ }
+
render () {
+ if (this.props.account.authenticated) {
+ return this.renderAccount()
+ } else {
+ return this.renderLoginForm()
+ }
+ }
+
+ renderAccount() {
+ let button = null
let message = null
+ let body = null
+ let header = <h1>{this.props.account.address}</h1>
+
if (this.state.error) {
// style may be: success, warning, danger, info
message = (
<Alert bsStyle="danger">{this.state.error}</Alert>
)
}
-
- if (this.props.account.authenticated) {
- let button = null
- if (this.state.loading) {
- button = <Button disabled={true}><Spinner /></Button>
- } else {
- button = <Button onClick={this.logout}>Log Out</Button>
- }
- return (
- <SectionLayout icon="user" buttons={button} status="on">
- <h1>{this.props.account.address}</h1>
- {message}
- </SectionLayout>
- )
+ if (this.state.expanded) {
+ body = <UserPasswordForm account={this.props.account} />
+ }
+ if (this.state.loading) {
+ button = <Button disabled={true}><Spinner /></Button>
} else {
- let address = null
- if (this.props.account.userpart) {
- address = this.props.account.address
- }
- return (
- <SectionLayout icon="user" className="wide-margin">
- <Login
- onLogin={this.props.onLogin}
- domain={this.props.account.domain}
- address={address}
- />
- </SectionLayout>
- )
+ button = <Button onClick={this.logout}>Log Out</Button>
+ }
+
+ return (
+ <SectionLayout icon="user" buttons={button} status="on"
+ onExpand={this.expand} header={header} body={body} message={message} />
+ )
+ }
+
+ renderLoginForm() {
+ let address = null
+ if (this.props.account.userpart) {
+ address = this.props.account.address
}
+ let header = (
+ <Login
+ onLogin={this.props.onLogin}
+ domain={this.props.account.domain}
+ address={address}
+ />
+ )
+ return (
+ <SectionLayout icon="user" className="wide-margin" header={header}/>
+ )
}
+
}
diff --git a/ui/app/components/password_field.js b/ui/app/components/password_field.js
new file mode 100644
index 0000000..8397967
--- /dev/null
+++ b/ui/app/components/password_field.js
@@ -0,0 +1,112 @@
+//
+// A validating password field, with a label and error messages.
+//
+
+import React from 'react'
+import { FormGroup, ControlLabel, FormControl, HelpBlock} from 'react-bootstrap'
+import Validate from 'lib/validate'
+
+export default class PasswordField extends React.Component {
+
+ static get defaultProps() {return{
+ id: null, // required. controlId of the element
+ label: "Password",
+ onChange: null, // callback passed current password
+ validationMode: "crack", // one of 'none', 'match', 'crack'
+ matchText: null, // used if validationMode == 'match'
+ }}
+
+ constructor(props) {
+ super(props)
+ this.state = {
+ password: null, // password value
+ passwordState: null, // password validation state
+ passwordError: false, // password help message
+ }
+ this.keypress = this.keypress.bind(this)
+ }
+
+ componentDidMount() {
+ if (this.props.validationMode == 'crack') {
+ Validate.loadPasswdLib()
+ }
+ }
+
+ render() {
+ let passwordHelp = null
+
+ if (this.state.passwordError) {
+ passwordHelp = <HelpBlock>{this.state.passwordError}</HelpBlock>
+ }
+
+ return (
+ <FormGroup controlId={this.props.id} validationState={this.state.passwordState}>
+ <ControlLabel>{this.props.label}</ControlLabel>
+ <FormControl
+ type="password"
+ ref="password"
+ value={this.state.password || ""}
+ onChange={this.keypress} />
+ {this.state.passwordState == 'success' ? null : <FormControl.Feedback/>}
+ {passwordHelp}
+ </FormGroup>
+ )
+ }
+
+ keypress(e) {
+ let password = e.target.value
+ if (this.props.onChange) {
+ this.props.onChange(password)
+ }
+ this.setState({password: password})
+ if (this.props.validationMode == 'crack') {
+ if (password.length > 0) {
+ this.validateCrack(password)
+ } else {
+ this.setState({
+ passwordState: null,
+ passwordError: null
+ })
+ }
+ } else if (this.props.validationMode == 'match') {
+ this.validateMatch(password)
+ }
+ }
+
+ validateCrack(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
+ })
+ }
+
+ validateMatch(password) {
+ if (this.props.matchText) {
+ if (password != this.props.matchText) {
+ this.setState({
+ passwordState: 'error',
+ passwordError: "Does not match"
+ })
+ } else {
+ this.setState({
+ passwordState: 'success',
+ passwordError: null
+ })
+ }
+ }
+ }
+
+}
diff --git a/ui/app/lib/bitmask.js b/ui/app/lib/bitmask.js
index 71a34f4..10e678e 100644
--- a/ui/app/lib/bitmask.js
+++ b/ui/app/lib/bitmask.js
@@ -41,7 +41,7 @@ var bitmask = function(){
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));
@@ -49,7 +49,7 @@ var bitmask = function(){
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);
@@ -58,11 +58,11 @@ var bitmask = function(){
reject(Error(req.statusText));
}
};
-
+
req.onerror = function() {
reject(Error("Network Error"));
};
-
+
req.send(data);
});
};
@@ -79,8 +79,8 @@ var bitmask = function(){
function event_polling() {
call(['events', 'poll']).then(function(response) {
if (response !== null) {
- evnt = response[0];
- content = response[1];
+ var evnt = response[0];
+ var content = response[1];
if (evnt in event_handlers) {
event_handlers[evnt](evnt, content);
}
@@ -176,7 +176,7 @@ var bitmask = function(){
uid = "";
}
return call(['bonafide', 'user', 'logout', uid]);
- }
+ },
/**
* Change password
@@ -187,7 +187,7 @@ var bitmask = function(){
*/
update: function(uid, current_password, new_password) {
return call(['bonafide', 'user', 'update', uid, current_password, new_password]);
- },
+ }
}
},
diff --git a/ui/app/lib/common.js b/ui/app/lib/common.js
index 14a30c3..d1485ce 100644
--- a/ui/app/lib/common.js
+++ b/ui/app/lib/common.js
@@ -4,4 +4,6 @@
import React from 'react'
-window.elem = React.createElement \ No newline at end of file
+window.elem = React.createElement
+
+window.log = console.log \ No newline at end of file
diff --git a/ui/app/models/account.js b/ui/app/models/account.js
index 04c8163..0ffdb07 100644
--- a/ui/app/models/account.js
+++ b/ui/app/models/account.js
@@ -114,7 +114,6 @@ export default class Account {
static active() {
return bitmask.bonafide.user.active().then(
response => {
- console.log(response)
if (response.user == '<none>') {
return null
} else {
@@ -139,7 +138,6 @@ export default class Account {
static create(address, password) {
return bitmask.bonafide.user.create(address, password).then(
response => {
- console.log(response)
return new Account(address)
}
)