Add password reset and email verification, change nomenclature so that "verification...
authorNick Downing <nick@ndcode.org>
Sun, 16 Jan 2022 00:04:47 +0000 (11:04 +1100)
committerNick Downing <nick@ndcode.org>
Sun, 16 Jan 2022 01:27:31 +0000 (12:27 +1100)
_lib/navbar.jst
api/password_reset.json.jst [new file with mode: 0644]
api/sign_up/send_email_verification_link.json.jst [moved from api/sign_up/send_verification_email.json.jst with 98% similarity]
api/sign_up/verify_email.json.jst
api/verify_password.json.jst [new file with mode: 0644]
my_account/password_reset/index.html.jst [new file with mode: 0644]
my_account/send_verification_email/index.html.jst
my_account/sign_up/index.html.jst
my_account/verify_password/index.html.jst [new file with mode: 0644]

index ea1f056..c4bef68 100644 (file)
@@ -360,15 +360,24 @@ return async (env, head, body, scripts) => {
                   )
                 }
                 catch (error) {
-                  if (
-                    error instanceof Problem &&
-                      error.title === 'Email not yet verified'
-                  ) {
+                  let problem =
+                    error instanceof Problem ?
+                      error :
+                      new Problem(
+                        // title
+                        'Bad request',
+                        // details
+                        (error.stack || error.message).toString()
+                        // status
+                        400
+                      )
+
+                  if (problem.title === 'Email not yet verified') {
                     location.href = `/my_account/send_verification_email?email=${encodeURIComponent(email)}`
                     return
                   }
 
-                  document.getElementById('message-modal-message').textContent = error.message
+                  document.getElementById('message-modal-message').textContent = problem.detail
                   $('#sign-in-modal').modal('hide')
                   $('#message-modal').modal('show')
                   return
@@ -393,7 +402,19 @@ return async (env, head, body, scripts) => {
                   await sign_out()
                 }
                 catch (error) {
-                  document.getElementById('message-modal-message').textContent = error.message
+                  let problem =
+                    error instanceof Problem ?
+                      error :
+                      new Problem(
+                        // title
+                        'Bad request',
+                        // details
+                        (error.stack || error.message).toString()
+                        // status
+                        400
+                      )
+
+                  document.getElementById('message-modal-message').textContent = problem.detail
                   $('#sign-in-modal').modal('hide')
                   $('#message-modal').modal('show')
                   return
diff --git a/api/password_reset.json.jst b/api/password_reset.json.jst
new file mode 100644 (file)
index 0000000..6f778fc
--- /dev/null
@@ -0,0 +1,99 @@
+let crypto = require('crypto')
+let logjson = (await import('@ndcode/logjson')).default
+let XDate = require('xdate')
+
+return async env => {
+  let globals = await env.site.get_json('/_config/globals.json')
+  let nodemailer_noreply = await env.site.get_nodemailer(
+    '/_config/nodemailer_noreply.json'
+  )
+  let post_request = await _require('/_lib/post_request.jst')
+  let session_cookie = await _require('/_lib/session_cookie.jst')
+  let Problem = await _require('/_lib/Problem.jst')
+
+  post_request(
+    // env
+    env,
+    // endpoint
+    '/api/password_reset.json',
+    // handler
+    async (email, password) => {
+      // coerce and/or validate
+      email = email.slice(0, 256).toLowerCase()
+      password = password.slice(0, 256)
+      if (email.length === 0 || password.length < 8)
+        throw new Problem(
+          'Bad request',
+          'Minimum length check failed',
+          400
+        )
+
+      let transaction = await env.site.database.Transaction()
+      try {
+        // initialize env.session_key, set cookie in env.response
+        await session_cookie(env, transaction)
+
+        let account = await (
+          await (
+            await transaction.get({})
+          ).get('accounts', {})
+        ).get(email)
+        if (account === undefined)
+          throw new Problem(
+            'Account does not exist',
+            `Please create the account for "${email}" before attempting to reset its password.`
+            421
+          )
+
+        let link_code = crypto.randomBytes(16).toString('hex')
+        let expires = new XDate()
+        expires.addDays(1)
+        account.set(
+          'verify_password',
+          transaction.json_to_logjson(
+            {
+              password,
+              link_code,
+              expires: expires.getTime()
+            }
+          )
+        )
+
+        let given_names = await logjson.logjson_to_json(
+          await account.get('given_names', '')
+        )
+        let family_name = await logjson.logjson_to_json(
+          await account.get('family_name', '')
+        )
+        let name =
+          family_name.length ? `${given_names} ${family_name}` : given_names
+
+        await nodemailer_noreply.sendMail(
+          {
+            from: globals.noreply_from,
+            to: `${name} <${email}>`,
+            subject: 'Password reset',
+            text: `Dear ${given_names},
+
+We have received a request to reset the account password for your email address.
+
+If this request is valid, please verify the new password by visiting the below link:
+${globals.site_url}/my_account/verify_password/index.html?email=${encodeURIComponent(email)}&link_code=${encodeURIComponent(link_code)}
+
+The link is valid for 24 hours.
+
+Thanks,
+${globals.noreply_signature}
+`
+          }
+        )
+
+        await transaction.commit()
+      }
+      catch (error) {
+        transaction.rollback()
+        throw error
+      }
+    }
+  )
+}
@@ -40,7 +40,7 @@ return async env => {
         if (account === undefined)
           throw new Problem(
             'Account does not exist',
-            `Please create the account for "${email}" before attempting to send a verification email.`
+            `Please create the account for "${email}" before attempting to send a email verification link.`
             421
           )
 
index dc70f77..d7f915c 100644 (file)
@@ -37,7 +37,7 @@ return async env => {
         if (account === undefined)
           throw new Problem(
             'Account does not exist',
-            `Please create the account for "${email}" before attempting to verify the email link.`
+            `Please create the account for "${email}" before attempting to verify the email verification link.`
             421
           )
 
@@ -61,7 +61,7 @@ return async env => {
         )
           throw new Problem(
             'Link code missing',
-            `Link code for email "${email}" does not exist or has expired.`,
+            `Email verification link code for account "${email}" does not exist or has expired.`,
             423
           )
         if (
@@ -71,7 +71,7 @@ return async env => {
         )
           throw new Problem(
             'Link code mismatch',
-            `Provided link code "${link_code}" does not match expected value.`,
+            `Provided email verification link code "${link_code}" does not match expected value.`,
             423
           )
 
diff --git a/api/verify_password.json.jst b/api/verify_password.json.jst
new file mode 100644 (file)
index 0000000..3e58569
--- /dev/null
@@ -0,0 +1,78 @@
+let crypto = require('crypto')
+let logjson = (await import('@ndcode/logjson')).default
+let XDate = require('xdate')
+
+return async env => {
+  let post_request = await _require('/_lib/post_request.jst')
+  let session_cookie = await _require('/_lib/session_cookie.jst')
+  let Problem = await _require('/_lib/Problem.jst')
+
+  post_request(
+    // env
+    env,
+    // endpoint
+    '/api/verify_password.json',
+    // handler
+    async (email, link_code) => {
+      // coerce and/or validate
+      email = email.slice(0, 256).toLowerCase()
+      link_code = link_code.slice(0, 256).toLowerCase()
+      if (email.length === 0 || link_code.length < 32)
+        throw new Problem(
+          'Bad request',
+          'Minimum length check failed',
+          400
+        )
+
+      let transaction = await env.site.database.Transaction()
+      try {
+        // initialize env.session_key, set cookie in env.response
+        await session_cookie(env, transaction)
+
+        let account = await (
+          await (
+            await transaction.get({})
+          ).get('accounts', {})
+        ).get(email)
+        if (account === undefined)
+          throw new Problem(
+            'Account does not exist',
+            `Please create the account for "${email}" before attempting to verify the password reset link.`
+            421
+          )
+
+        let verify_password = await account.get('verify_password')
+        if (
+          verify_password === undefined ||
+            XDate.now() >= await logjson.logjson_to_json(
+              await verify_password.get('expires')
+            )
+        )
+          throw new Problem(
+            'Link code missing',
+            `Password reset link code for account "${email}" does not exist or has expired.`,
+            423
+          )
+        if (
+          link_code !== await logjson.logjson_to_json(
+            await verify_password.get('link_code')
+          )
+        )
+          throw new Problem(
+            'Link code mismatch',
+            `Provided password reset link code "${link_code}" does not match expected value.`,
+            423
+          )
+
+        await account.delete('verify_password')
+        await account.set('password', await verify_password.get('password'))
+
+        await transaction.commit()
+      }
+      catch (error) {
+        transaction.rollback()
+        throw error
+      }
+    }
+  )
+}
diff --git a/my_account/password_reset/index.html.jst b/my_account/password_reset/index.html.jst
new file mode 100644 (file)
index 0000000..0295275
--- /dev/null
@@ -0,0 +1,195 @@
+let logjson = (await import('@ndcode/logjson')).default
+
+return async env => {
+  let breadcrumbs = await _require('/_lib/breadcrumbs.jst')
+  let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
+  let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
+  let navbar = await _require('/_lib/navbar.jst')
+  let session_cookie = await _require('/_lib/session_cookie.jst')
+
+  // preload draft details if any
+  let details = {}
+  if (Object.prototype.hasOwnProperty.call(env.parsed_url.query, 'email'))
+    details.email = decodeURIComponent(env.parsed_url.query.email)
+  console.log('details', JSON.stringify(details))
+
+  await navbar(
+    env,
+    // head
+    async _out => {},
+    // body
+    async _out => {
+      await breadcrumbs(env, _out)
+
+      p {'To reset your password, please enter new details below and we will send you a password reset link.'}
+
+      div.accordion#accordion.mb-5(role="tablist" aria-multiselectable="true") {
+        div.card#step-1 {
+          div.card-header#step-1-heading(role="tab") {
+            span#step-1-tick(style="display: none;") {
+              span.icon-color.pr-3 {_out.push(icon_tick)}
+            }
+            span#step-1-cross(style="display: none;") {
+              span.icon-color.pr-3 {_out.push(icon_cross)}
+            }
+            //span#step-1-spinner(style="display: none;") {
+            //  span.icon-color.pr-3 {
+            //    div.spinner-border(role="status") {
+            //      span.sr-only {'Loading...'}
+            //    }
+            //  }
+            //}
+            a.h5(data-toggle="collapse" data-parent="#accordion" href="#step-1-collapse" aria-expanded="true" aria-controls="step-1-collapse") {
+              'Account details'
+            }
+          }
+          div#step-1-collapse.collapse.show(role="tabpanel" aria-labelledby="step-1-heading" data-parent="#accordion") {
+            div.card-body {
+              div.row {
+                div.col-md-6 {
+                  div.form-group {
+                   label.form-label(for="email") {'Email *'}
+                    input.form-control#email(type="email" value=details.email || '' placeholder="Account email address" required="required" maxlength=256) {}
+                  }
+                }
+                div.col-md-6 {
+                  div.form-group {
+                   label.form-label(for="password") {'Password *'}
+                    input.form-control#password(type="password" placeholder="New password" required="required" minlength=8 maxlength=256) {}
+                  }
+                }
+              }
+
+              input.btn.btn-success#step-1-continue(type="button" value="Continue") {}
+              p.'mt-3'.mb-0 {'* This field is required.'}
+            }
+          }
+        }
+        div.card#step-2 {
+          div.card-header#step-2-heading(role="tab") {
+            span#step-2-tick(style="display: none;") {
+              span.icon-color.pr-3 {_out.push(icon_tick)}
+            }
+            span#step-2-cross(style="display: none;") {
+              span.icon-color.pr-3 {_out.push(icon_cross)}
+            }
+            span#step-2-spinner(style="display: none;") {
+              span.icon-color.pr-3 {
+                div.spinner-border(role="status") {
+                  span.sr-only {'Loading...'}
+                }
+              }
+            }
+            a.h5.collapsed(data-toggle="collapse" data-parent="#accordion" href="#step-2-collapse" aria-expanded="false" aria-controls="step-2-collapse") {
+              'Send password reset link'
+            }
+          }
+          div#step-2-collapse.collapse(role="tabpanel" aria-labelledby="step-2-heading" data-parent="#accordion") {
+            div.card-body {
+              p#step-2-message {'Please enter account details first.'}
+
+              input.btn.btn-outline-secondary#step-2-back(type="button" value="Back") {}
+              input.btn.btn-outline-secondary.ml-3#step-2-resend-email(type="button" value="Re-send email") {}
+            }
+          }
+        }
+      }
+    },
+    // scripts
+    async _out => {
+      script(src="/js/api_call.js") {}
+
+      script {
+        let password_reset = async (...arguments) => api_call(
+          '/api/password_reset.json',
+          ...arguments
+        )
+
+        let step_1 = async () => {
+          if (
+            !document.getElementById('email').reportValidity() || 
+              !document.getElementById('password').reportValidity()
+          ) {
+            $('#step-1-tick').hide()
+            $('#step-1-cross').show()
+            //$('#step-1-spinner').hide()
+            return false
+          }
+          $('#step-1-tick').show()
+          $('#step-1-cross').hide()
+          //$('#step-1-spinner').hide()
+          return true
+        }
+
+        let step_2 = async () => {
+          $('#step-2-tick').hide()
+          $('#step-2-cross').hide()
+          $('#step-2-spinner').show()
+          document.getElementById('step-1').scrollIntoView()
+
+          let email
+          try {
+            email = document.getElementById('email').value.slice(0, 256).toLowerCase()
+            await password_reset(
+              email,
+              document.getElementById('password').value.slice(0, 256)
+            )
+          }
+          catch (error) {
+            let problem =
+              error instanceof Problem ?
+                error :
+                new Problem(
+                  // title
+                  'Bad request',
+                  // details
+                  (error.stack || error.message).toString()
+                  // status
+                  400
+                )
+
+            $('#step-2-tick').hide()
+            $('#step-2-cross').show()
+            $('#step-2-spinner').hide()
+
+            document.getElementById('step-2-message').textContent = problem.detail
+            $('#step-2-collapse').collapse('show')
+            return false
+          }
+          $('#step-2-tick').show()
+          $('#step-2-cross').hide()
+          $('#step-2-spinner').hide()
+
+          document.getElementById('step-2-message').textContent = `Password reset link has been sent to "${email}". Please check your email for next steps.`
+          return true
+        }
+
+        document.addEventListener(
+          'DOMContentLoaded',
+          () => {
+            document.getElementById('step-1-continue').addEventListener(
+              'click',
+              async () => {
+                if (await step_1() && await step_2())
+                  $('#step-2-collapse').collapse('show')
+              }
+            )
+
+            document.getElementById('step-2-back').addEventListener(
+              'click',
+              () => {$('#step-1-collapse').collapse('show')}
+            )
+
+            document.getElementById('step-2-resend-email').addEventListener(
+              'click',
+              async () => {
+                if (await step_2())
+                  $('#step-3-collapse').collapse('show')
+              }
+            )
+          }
+        )
+      }
+    }
+  )
+}
index 5d7c960..9b5d588 100644 (file)
@@ -21,7 +21,7 @@ return async env => {
     async _out => {
       await breadcrumbs(env, _out)
 
-      p {'Please verify your email address before signing in. Check your email for next steps, or re-send the verification email below.'}
+      p {'Please verify your email address before signing in. Check your email for next steps, or re-send the email verification link below.'}
 
       div.accordion#accordion.mb-5(role="tablist" aria-multiselectable="true") {
         div.card#step-1 {
@@ -75,7 +75,7 @@ return async env => {
               }
             }
             a.h5.collapsed(data-toggle="collapse" data-parent="#accordion" href="#step-2-collapse" aria-expanded="false" aria-controls="step-2-collapse") {
-              'Send verification email'
+              'Send email verification link'
             }
           }
           div#step-2-collapse.collapse(role="tabpanel" aria-labelledby="step-2-heading" data-parent="#accordion") {
@@ -94,8 +94,8 @@ return async env => {
       script(src="/js/api_call.js") {}
 
       script {
-        let sign_up_send_verification_email = async (...arguments) => api_call(
-          '/api/sign_up/send_verification_email.json',
+        let sign_up_send_email_verification_link = async (...arguments) => api_call(
+          '/api/sign_up/send_email_verification_link.json',
           ...arguments
         )
 
@@ -121,7 +121,7 @@ return async env => {
           let email
           try {
             email = document.getElementById('email').value.slice(0, 256).toLowerCase()
-            await sign_up_send_verification_email(email)
+            await sign_up_send_email_verification_link(email)
           }
           catch (error) {
             let problem =
@@ -148,7 +148,7 @@ return async env => {
           $('#step-2-cross').hide()
           $('#step-2-spinner').hide()
 
-          document.getElementById('step-2-message').textContent = `Verification email has been sent to "${email}". Please check your email for next steps.`
+          document.getElementById('step-2-message').textContent = `Email verification link has been sent to "${email}". Please check your email for next steps.`
           return true
         }
 
index 4038b2e..179a407 100644 (file)
@@ -162,7 +162,7 @@ return async env => {
               }
             }
             a.h5.collapsed(data-toggle="collapse" data-parent="#accordion" href="#step-3-collapse" aria-expanded="false" aria-controls="step-3-collapse") {
-              'Send verification email'
+              'Send email verification link'
             }
           }
           div#step-3-collapse.collapse(role="tabpanel" aria-labelledby="step-3-heading" data-parent="#accordion") {
@@ -193,8 +193,8 @@ return async env => {
           '/api/sign_up/set_draft.json',
           ...arguments
         )
-        let sign_up_send_verification_email = async (...arguments) => api_call(
-          '/api/sign_up/send_verification_email.json',
+        let sign_up_send_email_verification_link = async (...arguments) => api_call(
+          '/api/sign_up/send_email_verification_link.json',
           ...arguments
         )
 
@@ -291,7 +291,7 @@ return async env => {
           document.getElementById('step-3').scrollIntoView()
 
           try {
-            await sign_up_send_verification_email(step_2_details.email)
+            await sign_up_send_email_verification_link(step_2_details.email)
           }
           catch (error) {
             let problem =
@@ -318,7 +318,7 @@ return async env => {
           $('#step-3-cross').hide()
           $('#step-3-spinner').hide()
 
-          document.getElementById('step-3-message').textContent = `Verification email has been sent to "${step_2_details.email}". Please check your email for next steps.`
+          document.getElementById('step-3-message').textContent = `Email verification link has been sent to "${step_2_details.email}". Please check your email for next steps.`
           return true
         }
 
diff --git a/my_account/verify_password/index.html.jst b/my_account/verify_password/index.html.jst
new file mode 100644 (file)
index 0000000..67d441c
--- /dev/null
@@ -0,0 +1,202 @@
+let logjson = (await import('@ndcode/logjson')).default
+
+return async env => {
+  let breadcrumbs = await _require('/_lib/breadcrumbs.jst')
+  let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
+  let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
+  let navbar = await _require('/_lib/navbar.jst')
+  let session_cookie = await _require('/_lib/session_cookie.jst')
+
+  // preload draft details if any
+  let details = {}
+  if (Object.prototype.hasOwnProperty.call(env.parsed_url.query, 'email'))
+    details.email = decodeURIComponent(env.parsed_url.query.email)
+  if (
+     Object.prototype.hasOwnProperty.call(
+      env.parsed_url.query,
+      'link_code'
+     )
+   )
+    details.link_code =
+      decodeURIComponent(env.parsed_url.query.link_code)
+  console.log('details', JSON.stringify(details))
+
+  await navbar(
+    env,
+    // head
+    async _out => {},
+    // body
+    async _out => {
+      await breadcrumbs(env, _out)
+
+      p {'You will need to verify your new password via an emailed link before you can use it to sign in to your account.'}
+
+      div.accordion#accordion.mb-5(role="tablist" aria-multiselectable="true") {
+        div.card#step-1 {
+          div.card-header#step-1-heading(role="tab") {
+            span#step-1-tick(style="display: none;") {
+              span.icon-color.pr-3 {_out.push(icon_tick)}
+            }
+            span#step-1-cross(style="display: none;") {
+              span.icon-color.pr-3 {_out.push(icon_cross)}
+            }
+            //span#step-1-spinner(style="display: none;") {
+            //  span.icon-color.pr-3 {
+            //    div.spinner-border(role="status") {
+            //      span.sr-only {'Loading...'}
+            //    }
+            //  }
+            //}
+            a.h5(data-toggle="collapse" data-parent="#accordion" href="#step-1-collapse" aria-expanded="true" aria-controls="step-1-collapse") {
+              'Link details'
+            }
+          }
+          div#step-1-collapse.collapse.show(role="tabpanel" aria-labelledby="step-1-heading" data-parent="#accordion") {
+            div.card-body {
+              div.row {
+                div.col-md-6 {
+                  div.form-group {
+                   label.form-label(for="email") {'Email *'}
+                    input.form-control#email(type="email" value=details.email || '' placeholder="Account email address" required="required" maxlength=256) {}
+                  }
+                }
+                div.col-md-6 {
+                  div.form-group {
+                    label.form-label(for="link-code") {'Link code *'}
+                    input.form-control#link-code(type="text" value=details.link_code || '' placeholder="Type the code from the email link" required="required" minlength=32 maxlength=32) {}
+                  }
+                }
+              }
+
+              input.btn.btn-success#step-1-continue(type="button" value="Continue") {}
+              p.'mt-3'.mb-0 {'* These fields are required.'}
+            }
+          }
+        }
+        div.card#step-2 {
+          div.card-header#step-2-heading(role="tab") {
+            span#step-2-tick(style="display: none;") {
+              span.icon-color.pr-3 {_out.push(icon_tick)}
+            }
+            span#step-2-cross(style="display: none;") {
+              span.icon-color.pr-3 {_out.push(icon_cross)}
+            }
+            span#step-2-spinner(style="display: none;") {
+              span.icon-color.pr-3 {
+                div.spinner-border(role="status") {
+                  span.sr-only {'Loading...'}
+                }
+              }
+            }
+            a.h5.collapsed(data-toggle="collapse" data-parent="#accordion" href="#step-2-collapse" aria-expanded="false" aria-controls="step-2-collapse") {
+              'Verify password'
+            }
+          }
+          div#step-2-collapse.collapse(role="tabpanel" aria-labelledby="step-2-heading" data-parent="#accordion") {
+            div.card-body {
+              p#step-2-message {'Please enter link details first.'}
+
+              input.btn.btn-outline-secondary#step-2-back(type="button" value="Back") {}
+              input.btn.btn-outline-secondary.ml-2#step-2-sign-in(type="button" value="Sign in") {}
+            }
+          }
+        }
+      }
+    },
+    // scripts
+    async _out => {
+      script(src="/js/api_call.js") {}
+
+      script {
+        let verify_password = async (...arguments) => api_call(
+          '/api/verify_password.json',
+          ...arguments
+        )
+
+        let step_1 = async () => {
+          if (
+            !document.getElementById('email').reportValidity() ||
+              !document.getElementById('link-code').reportValidity()
+          ) {
+            $('#step-1-tick').hide()
+            $('#step-1-cross').show()
+            //$('#step-1-spinner').hide()
+            return false
+          }
+          $('#step-1-tick').show()
+          $('#step-1-cross').hide()
+          //$('#step-1-spinner').hide()
+          return true
+        }
+
+        let step_2 = async () => {
+          $('#step-2-tick').hide()
+          $('#step-2-cross').hide()
+          $('#step-2-spinner').show()
+          document.getElementById('step-1').scrollIntoView()
+
+          let email
+          try {
+            email = document.getElementById('email').value.slice(0, 256).toLowerCase()
+            await verify_password(
+              // email
+              email,
+              // link_code
+              document.getElementById('link-code').value.slice(0, 32).toLowerCase()
+            )
+          }
+          catch (error) {
+            let problem =
+              error instanceof Problem ?
+                error :
+                new Problem(
+                  // title
+                  'Bad request',
+                  // details
+                  (error.stack || error.message).toString()
+                  // status
+                  400
+                )
+
+            $('#step-2-tick').hide()
+            $('#step-2-cross').show()
+            $('#step-2-spinner').hide()
+
+            document.getElementById('step-2-message').textContent = problem.detail
+            $('#step-2-collapse').collapse('show')
+            return false
+          }
+          $('#step-2-tick').show()
+          $('#step-2-cross').hide()
+          $('#step-2-spinner').hide()
+
+          document.getElementById('step-2-message').textContent = `New password for "${email}" has been verified. You can now sign in.`
+          return true
+        }
+
+        document.addEventListener(
+          'DOMContentLoaded',
+          () => {
+            document.getElementById('step-1-continue').addEventListener(
+              'click',
+              async () => {
+                if (await step_1() && await step_2())
+                  $('#step-2-collapse').collapse('show')
+              }
+            )
+
+            document.getElementById('step-2-back').addEventListener(
+              'click',
+              () => {$('#step-1-collapse').collapse('show')}
+            )
+
+            document.getElementById('step-2-sign-in').addEventListener(
+              'click',
+              () => {document.getElementById('sign-in').click()}
+            )
+          }
+        )
+      }
+    }
+  )
+}