diff options
Diffstat (limited to 'ui/app')
| -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 | ||||
| -rw-r--r-- | ui/app/lib/bitmask.js | 16 | ||||
| -rw-r--r-- | ui/app/lib/common.js | 4 | ||||
| -rw-r--r-- | ui/app/models/account.js | 2 | 
9 files changed, 415 insertions, 72 deletions
diff --git a/ui/app/components/login.js b/ui/app/components/login.js index 059551f9..00f52d5a 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 4e0ecb05..2e979138 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 e7c6f2ab..10c1bc1d 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 00000000..5e26b198 --- /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 acaea234..317f9931 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 00000000..8397967f --- /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 71a34f4a..10e678ee 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 14a30c32..d1485ce7 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 04c8163e..0ffdb07e 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)        }      )  | 
