github.com/resonatecoop/id@v1.1.0-43/frontend/src/components/forms/signup.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 isEmail = require('validator/lib/isEmail') 8 const isEmpty = require('validator/lib/isEmpty') 9 const isLength = require('validator/lib/isLength') 10 const validateFormdata = require('validate-formdata') 11 const PasswordMeter = require('../password-meter') 12 const CountrySelect = require('../select-country-list') 13 const zxcvbnAsync = require('zxcvbn-async') 14 const RoleSwitcher = require('./roleSwitcher') 15 16 class Signup extends Component { 17 constructor (id, state, emit) { 18 super(id) 19 20 this.emit = emit 21 this.state = state 22 23 this.local = state.components[id] = Object.create({ 24 machine: nanostate.parallel({ 25 request: nanostate('idle', { 26 idle: { start: 'loading' }, 27 loading: { resolve: 'data', reject: 'error', reset: 'idle' }, 28 data: { reset: 'idle', start: 'loading' }, 29 error: { reset: 'idle', start: 'loading' } 30 }), 31 loader: nanostate('off', { 32 on: { toggle: 'off' }, 33 off: { toggle: 'on' } 34 }) 35 }) 36 }) 37 38 this.local.error = {} 39 40 this.local.machine.on('request:error', () => { 41 if (this.element) this.rerender() 42 }) 43 44 this.local.machine.on('request:loading', () => { 45 if (this.element) this.rerender() 46 }) 47 48 this.local.machine.on('loader:toggle', () => { 49 if (this.element) this.rerender() 50 }) 51 52 this.local.machine.transitions.request.event('error', nanostate('error', { 53 error: { start: 'loading' } 54 })) 55 56 this.local.machine.on('request:noResults', () => { 57 if (this.element) this.rerender() 58 }) 59 60 this.local.machine.transitions.request.event('noResults', nanostate('noResults', { 61 noResults: { start: 'loading' } 62 })) 63 64 this.validator = validateFormdata() 65 this.local.form = this.validator.state 66 this.local.role = 'user' 67 this.local.roleId = 6 68 } 69 70 createElement (props) { 71 const message = { 72 loading: html`<p class="status w-100 pa1">Loading...</p>`, 73 error: html`<p class="status bg-yellow w-100 black pa1">${this.local.error.message}</p>`, 74 data: '', 75 noResults: html`<p class="status bg-yellow w-100 black pa1">An error occured.</p>` 76 }[this.local.machine.state.request] 77 78 return html` 79 <div class="flex flex-column flex-auto"> 80 ${message} 81 ${this.state.cache(Form, 'signup-form').render({ 82 id: 'signup', 83 method: 'POST', 84 action: '', 85 buttonText: 'Sign up', 86 altButton: html` 87 <p class="f5 lh-copy">Already have an account? <a class="link b" href="/login">Log In</a>.</p> 88 `, 89 validate: (props) => { 90 this.validator.validate(props.name, props.value) 91 this.rerender() 92 }, 93 form: this.local.form || { 94 changed: false, 95 valid: true, 96 pristine: {}, 97 required: {}, 98 values: {}, 99 errors: {} 100 }, 101 fields: [ 102 { 103 component: this.state.cache(RoleSwitcher, 'role-switcher').render({ 104 value: this.local.role, 105 onChangeCallback: async (value) => { 106 this.local.role = value 107 } 108 }) 109 }, 110 { 111 type: 'email', 112 placeholder: 'E-mail' 113 }, 114 { 115 type: 'password', 116 placeholder: 'Password', 117 help: (value) => { 118 return this.state.cache(PasswordMeter, 'password-meter').render({ 119 password: value 120 }) 121 } 122 }, 123 { 124 component: this.state.cache(CountrySelect, 'join-country-select').render({ 125 validator: this.validator, 126 form: this.local.form || { 127 changed: false, 128 valid: true, 129 pristine: {}, 130 required: {}, 131 values: {}, 132 errors: {} 133 }, 134 required: true, 135 onchange: (e) => { 136 // something changed 137 } 138 }) 139 } 140 ], 141 submit: async (data) => { 142 if (this.local.machine.state === 'loading') { 143 return 144 } 145 146 const loaderTimeout = setTimeout(() => { 147 this.local.machine.emit('loader:toggle') 148 }, 1000) 149 150 try { 151 this.local.machine.emit('request:start') 152 153 let response = await fetch('') 154 155 const csrfToken = response.headers.get('X-CSRF-Token') 156 157 response = await fetch('', { 158 method: 'POST', 159 credentials: 'include', 160 headers: { 161 Accept: 'application/json', 162 'X-CSRF-Token': csrfToken 163 }, 164 body: new URLSearchParams({ 165 email: data.email.value, 166 password: data.password.value, 167 country: data.country.value, 168 role: this.local.role 169 }) 170 }) 171 172 const isRedirected = response.redirected 173 174 if (isRedirected) { 175 window.location.href = response.url 176 } 177 178 this.local.machine.state.loader === 'on' && this.local.machine.emit('loader:toggle') 179 180 const status = response.status 181 const contentType = response.headers.get('content-type') 182 183 if (status >= 400 && contentType && contentType.indexOf('application/json') !== -1) { 184 const { error } = await response.json() 185 this.local.error.message = error 186 return this.local.machine.emit('request:error') 187 } 188 189 if (status === 201) { 190 const redirectURL = new URL('/login', 'http://localhost') 191 192 redirectURL.search = new URLSearchParams({ 193 confirm: true, 194 login_redirect_uri: '/web/account' 195 }) 196 197 this.emit(this.state.events.PUSHSTATE, redirectURL.pathname + redirectURL.search) 198 } 199 200 this.local.machine.emit('request:resolve') 201 } catch (err) { 202 this.local.error.message = err.message 203 this.local.machine.emit('request:reject') 204 this.emit('error', err) 205 } finally { 206 clearTimeout(loaderTimeout) 207 } 208 } 209 })} 210 </div> 211 ` 212 } 213 214 load () { 215 const zxcvbn = zxcvbnAsync.load({ 216 sync: true, 217 libUrl: 'https://cdn.jsdelivr.net/npm/zxcvbn@4.4.2/dist/zxcvbn.js', 218 libIntegrity: 'sha256-9CxlH0BQastrZiSQ8zjdR6WVHTMSA5xKuP5QkEhPNRo=' 219 }) 220 221 this.validator.field('email', (data) => { 222 if (isEmpty(data)) { 223 return new Error('Please tell us your email address') 224 } 225 if (!isEmail(data)) { 226 return new Error('This is not a valid email address') 227 } 228 }) 229 this.validator.field('password', (data) => { 230 if (isEmpty(data)) { 231 return new Error('A strong password is very important') 232 } 233 if (!isLength(data, { min: 9 })) { 234 return new Error('Password length should not be less than 9 characters') 235 } 236 const { score, feedback } = zxcvbn(data) 237 if (score < 3) { 238 return new Error(feedback.warning || (feedback.suggestions.length ? feedback.suggestions[0] : 'Password is too weak')) 239 } 240 if (!isLength(data, { max: 72 })) { 241 return new Error('Password length should not be more than 72 characters') 242 } 243 }) 244 } 245 246 update () { 247 return false 248 } 249 } 250 251 module.exports = Signup