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