github.com/resonatecoop/id@v1.1.0-43/frontend/src/components/forms/passwordUpdate.js (about)

     1  /* global fetch */
     2  
     3  const html = require('choo/html')
     4  const Component = require('choo/component')
     5  const Form = require('./generic')
     6  const logger = require('nanologger')
     7  const log = logger('form:updatePassword')
     8  
     9  const isEmpty = require('validator/lib/isEmpty')
    10  const isLength = require('validator/lib/isLength')
    11  const validateFormdata = require('validate-formdata')
    12  const nanostate = require('nanostate')
    13  const PasswordMeter = require('../password-meter')
    14  const zxcvbnAsync = require('zxcvbn-async')
    15  
    16  class UpdatePasswordForm extends Component {
    17    constructor (id, state, emit) {
    18      super(id)
    19  
    20      this.emit = emit
    21      this.state = state
    22  
    23      this.local = Object.create({
    24        machine: nanostate.parallel({
    25          form: nanostate('idle', {
    26            idle: { submit: 'submitted' },
    27            submitted: { valid: 'data', invalid: 'error' },
    28            data: { reset: 'idle', submit: 'submitted' },
    29            error: { reset: 'idle', submit: 'submitted', invalid: 'error' }
    30          }),
    31          request: nanostate('idle', {
    32            idle: { start: 'loading' },
    33            loading: { resolve: 'data', reject: 'error' },
    34            data: { start: 'loading' },
    35            error: { start: 'loading', stop: 'idle' }
    36          })
    37        })
    38      })
    39  
    40      this.local.data = {}
    41      this.local.error = {}
    42  
    43      this.local.machine.on('form:reset', () => {
    44        this.validator = validateFormdata()
    45        this.local.form = this.validator.state
    46      })
    47  
    48      this.local.machine.on('request:start', () => {})
    49  
    50      this.local.machine.on('request:reject', () => {
    51        this.emit('notify', { type: 'error', message: this.local.error.message || 'Something went wrong' })
    52      })
    53  
    54      this.local.machine.on('request:resolve', () => {
    55        this.emit('notify', { type: 'success', message: 'Password changed!' })
    56      })
    57  
    58      this.local.machine.on('form:valid', async () => {
    59        log.info('Form is valid')
    60  
    61        try {
    62          this.local.machine.emit('request:start')
    63  
    64          let response = await fetch('')
    65  
    66          const csrfToken = response.headers.get('X-CSRF-Token')
    67  
    68          response = await fetch('/password', {
    69            method: 'PUT',
    70            headers: {
    71              Accept: 'application/json',
    72              'X-CSRF-Token': csrfToken
    73            },
    74            body: new URLSearchParams({
    75              password: this.local.data.password,
    76              password_new: this.local.data.password_new,
    77              password_confirm: this.local.data.password_confirm
    78            })
    79          })
    80  
    81          const status = response.status
    82          const contentType = response.headers.get('content-type')
    83  
    84          if (status >= 400 && contentType && contentType.indexOf('application/json') !== -1) {
    85            const { error } = await response.json()
    86            this.local.error.message = error
    87            this.local.machine.emit('request:reject')
    88          } else {
    89            this.local.machine.emit('request:resolve')
    90          }
    91        } catch (err) {
    92          this.local.error.message = err.message
    93          this.local.machine.emit('request:reject')
    94        }
    95      })
    96  
    97      this.local.machine.on('form:invalid', () => {
    98        log.info('Form is invalid')
    99  
   100        const invalidInput = document.querySelector('.invalid')
   101  
   102        if (invalidInput) {
   103          invalidInput.focus({ preventScroll: false }) // focus to first invalid input
   104        }
   105      })
   106  
   107      this.local.machine.on('form:submit', () => {
   108        log.info('Form has been submitted')
   109  
   110        const form = this.element.querySelector('form')
   111  
   112        for (const field of form.elements) {
   113          const isRequired = field.required
   114          const name = field.name || ''
   115          const value = field.value || ''
   116  
   117          if (isRequired) {
   118            this.validator.validate(name, value)
   119          }
   120        }
   121  
   122        this.rerender()
   123  
   124        if (this.local.form.valid) {
   125          return this.local.machine.emit('form:valid')
   126        }
   127  
   128        return this.local.machine.emit('form:invalid')
   129      })
   130  
   131      this.validator = validateFormdata()
   132      this.local.form = this.validator.state
   133    }
   134  
   135    createElement (props) {
   136      return html`
   137        <div class="flex flex-column flex-auto pb6">
   138          ${this.state.cache(Form, 'password-update-form').render({
   139            id: 'password-update-form',
   140            method: 'POST',
   141            action: '',
   142            buttonText: 'Update my password',
   143            validate: (props) => {
   144              this.local.data[props.name] = props.value
   145              this.validator.validate(props.name, props.value)
   146              this.rerender()
   147            },
   148            form: this.local.form || {
   149              changed: false,
   150              valid: true,
   151              pristine: {},
   152              required: {},
   153              values: {},
   154              errors: {}
   155            },
   156            submit: () => {
   157              this.local.machine.emit('form:submit')
   158            },
   159            fields: [
   160              {
   161                type: 'password',
   162                id: 'password_current',
   163                autocomplete: 'on',
   164                name: 'password',
   165                placeholder: 'Current password'
   166              },
   167              {
   168                type: 'password',
   169                id: 'password_new',
   170                autocomplete: 'on',
   171                name: 'password_new',
   172                placeholder: 'New password',
   173                help: (value) => {
   174                  return this.state.cache(PasswordMeter, 'password-meter').render({
   175                    password: value
   176                  })
   177                }
   178              },
   179              {
   180                type: 'password',
   181                id: 'password_confirm',
   182                autocomplete: 'on',
   183                name: 'password_confirm',
   184                placeholder: 'Password confirmation'
   185              }
   186            ]
   187          })}
   188        </div>
   189      `
   190    }
   191  
   192    load () {
   193      const zxcvbn = zxcvbnAsync.load({
   194        sync: true,
   195        libUrl: 'https://cdn.jsdelivr.net/npm/zxcvbn@4.4.2/dist/zxcvbn.js',
   196        libIntegrity: 'sha256-9CxlH0BQastrZiSQ8zjdR6WVHTMSA5xKuP5QkEhPNRo='
   197      })
   198  
   199      this.validator.field('password', { required: !!this.local.token }, (data) => {
   200        if (isEmpty(data)) return new Error('Current password is required')
   201        if (/[À-ÖØ-öø-ÿ]/.test(data)) return new Error('Current password may contain unsupported characters. You should ask for a password reset.')
   202      })
   203      this.validator.field('password_new', (data) => {
   204        if (isEmpty(data)) return new Error('New password is required')
   205        if (data === this.local.data.password) return new Error('Current password and new password are identical')
   206        const { score, feedback } = zxcvbn(data)
   207        if (score < 3) {
   208          return new Error(feedback.warning || (feedback.suggestions.length ? feedback.suggestions[0] : 'Password is too weak'))
   209        }
   210        if (!isLength(data, { max: 72 })) {
   211          return new Error('Password length should not be more than 72 characters')
   212        }
   213      })
   214      this.validator.field('password_confirm', (data) => {
   215        if (isEmpty(data)) return new Error('Password confirmation is required')
   216        if (data !== this.local.data.password_new) return new Error('Password mismatch')
   217      })
   218    }
   219  
   220    update () {
   221      return false
   222    }
   223  }
   224  
   225  module.exports = UpdatePasswordForm