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

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