diff options
author | elijah <elijah@riseup.net> | 2016-09-29 17:00:47 -0700 |
---|---|---|
committer | elijah <elijah@riseup.net> | 2016-09-29 17:00:47 -0700 |
commit | 10b0b4462107ecebffab4ce3eb0435d3c1b2dd24 (patch) | |
tree | 14ae6e69e56f53eacb299d59b0f6a1beda3063b8 /ui/app/components | |
parent | e7281dd47f375c1b0a72ae85505319c4d87fb524 (diff) |
[feat] ui - allow users to change their passwords
Diffstat (limited to 'ui/app/components')
-rw-r--r-- | ui/app/components/login.js | 6 | ||||
-rw-r--r-- | ui/app/components/main_panel/main_panel.less | 91 | ||||
-rw-r--r-- | ui/app/components/main_panel/section_layout.js | 55 | ||||
-rw-r--r-- | ui/app/components/main_panel/user_password_form.js | 123 | ||||
-rw-r--r-- | ui/app/components/main_panel/user_section.js | 78 | ||||
-rw-r--r-- | ui/app/components/password_field.js | 112 |
6 files changed, 404 insertions, 61 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 + }) + } + } + } + +} |