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