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