Add a page to re-send email verification if signing in without verified email
authorNick Downing <nick@ndcode.org>
Sat, 15 Jan 2022 23:29:44 +0000 (10:29 +1100)
committerNick Downing <nick@ndcode.org>
Sat, 15 Jan 2022 23:34:32 +0000 (10:34 +1100)
_lib/navbar.jst
js/api_call.js.min
my_account/_menu.json
my_account/send_verification_email/index.html.jst [new file with mode: 0644]
my_account/sign_up/index.html.jst
my_account/verify_email/index.html.jst
search/index.html.jst

index c6ff55f..ea1f056 100644 (file)
@@ -360,6 +360,14 @@ return async (env, head, body, scripts) => {
                   )
                 }
                 catch (error) {
+                  if (
+                    error instanceof Problem &&
+                      error.title === 'Email not yet verified'
+                  ) {
+                    location.href = `/my_account/send_verification_email?email=${encodeURIComponent(email)}`
+                    return
+                  }
+
                   document.getElementById('message-modal-message').textContent = error.message
                   $('#sign-in-modal').modal('hide')
                   $('#message-modal').modal('show')
index 7ddced8..aa1db76 100644 (file)
@@ -1,12 +1,18 @@
+Problem = class {
+  constructor(title, detail, status) {
+    this.title = title
+    this.detail = detail
+    this.status = status
+  }
+}
+
 api_call = async (endpoint, ...arguments) => {
   let response = await fetch(
     endpoint,
-    {
-      method: 'POST',
-      body: JSON.stringify(arguments)
-    }
+    {method: 'POST', body: JSON.stringify(arguments)}
   )
+  let result = await response.json()
   if (!response.ok)
-    throw new Error((await response.json()).detail)
-  return /*await*/ response.json()
+    throw new Problem(result.title, result.detail, result.status)
+  return result
 }
index 5cf9006..30cfa1a 100644 (file)
@@ -5,6 +5,7 @@
       "name": "Sign up",
       "icon": "/_svg/icon_sign_up.svg"
     },
+    {"dir": "send_verification_email", "name": "Send verification email"},
     {"dir": "verify_email", "name": "Verify email"},
     {
       "dir": "password_reset",
diff --git a/my_account/send_verification_email/index.html.jst b/my_account/send_verification_email/index.html.jst
new file mode 100644 (file)
index 0000000..5d7c960
--- /dev/null
@@ -0,0 +1,183 @@
+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 {'Please verify your email address before signing in. Check your email for next steps, or re-send the verification email below.'}
+
+      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) {}
+                  }
+                }
+              }
+
+              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 verification email'
+            }
+          }
+          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 sign_up_send_verification_email = async (...arguments) => api_call(
+          '/api/sign_up/send_verification_email.json',
+          ...arguments
+        )
+
+        let step_1 = async () => {
+          if (!document.getElementById('email').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 sign_up_send_verification_email(email)
+          }
+          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 = `Verification email 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 894787c..4038b2e 100644 (file)
@@ -33,7 +33,7 @@ return async env => {
       p {'Signing up allows you to leave comments on our blog and receive communications from us.'}
 
       div.accordion#accordion.mb-5(role="tablist" aria-multiselectable="true") {
-        div.card {
+        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)}
@@ -118,7 +118,7 @@ return async env => {
             }
           }
         }
-        div.card {
+        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)}
@@ -146,7 +146,7 @@ return async env => {
             }
           }
         }
-        div.card {
+        div.card#step-3 {
           div.card-header#step-3-heading(role="tab") {
             span#step-3-tick(style="display: none;") {
               span.icon-color.pr-3 {_out.push(icon_tick)}
@@ -170,7 +170,7 @@ return async env => {
               p#step-3-message {'Please create your account first.'}
 
               input.btn.btn-outline-secondary#step-3-back(type="button" value="Back") {}
-              input.btn.btn-outline-secondary.ml-3#step-3-resend-email(type="button" value="Resend email") {}
+              input.btn.btn-outline-secondary.ml-3#step-3-resend-email(type="button" value="Re-send email") {}
             }
           }
         }
@@ -245,6 +245,8 @@ return async env => {
           $('#step-2-tick').hide()
           $('#step-2-cross').hide()
           $('#step-2-spinner').show()
+          document.getElementById('step-2').scrollIntoView()
+
           try {
             step_2_details = coerce_details()
             await sign_up_create_account(
@@ -254,19 +256,30 @@ return async env => {
               step_2_details
             )
           }
-          catch (e) {
+          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 = e.message
+            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 = `Your account with email "${step_2_details.email}" has been created.`
           return true
         }
@@ -275,15 +288,29 @@ return async env => {
           $('#step-3-tick').hide()
           $('#step-3-cross').hide()
           $('#step-3-spinner').show()
+          document.getElementById('step-3').scrollIntoView()
+
           try {
             await sign_up_send_verification_email(step_2_details.email)
           }
-          catch (e) {
+          catch (error) {
+            let problem =
+              error instanceof Problem ?
+                error :
+                new Problem(
+                  // title
+                  'Bad request',
+                  // details
+                  (error.stack || error.message).toString()
+                  // status
+                  400
+                )
+
             $('#step-3-tick').hide()
             $('#step-3-cross').show()
             $('#step-3-spinner').hide()
 
-            document.getElementById('step-3-message').textContent = e.message
+            document.getElementById('step-3-message').textContent = problem.detail
             $('#step-3-collapse').collapse('show')
             return false
           }
index dee5ab3..ffb3cca 100644 (file)
@@ -29,10 +29,10 @@ return async env => {
     async _out => {
       await breadcrumbs(env, _out)
 
-      p {'You will need to verify your email address via an email link before you can sign in to your account.'}
+      p {'You will need to verify your email address via an emailed link before you can sign in to your account.'}
 
       div.accordion#accordion.mb-5(role="tablist" aria-multiselectable="true") {
-        div.card {
+        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)}
@@ -73,7 +73,7 @@ return async env => {
             }
           }
         }
-        div.card {
+        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)}
@@ -94,7 +94,7 @@ return async env => {
           }
           div#step-2-collapse.collapse(role="tabpanel" aria-labelledby="step-2-heading" data-parent="#accordion") {
             div.card-body {
-              p#step-2-message {'Please enter email link details first.'}
+              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") {}
@@ -133,6 +133,8 @@ return async env => {
           $('#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()
@@ -143,12 +145,24 @@ return async env => {
               document.getElementById('link-code').value.slice(0, 32).toLowerCase()
             )
           }
-          catch (e) {
+          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 = e.message
+            document.getElementById('step-2-message').textContent = problem.detail
             $('#step-2-collapse').collapse('show')
             return false
           }
@@ -175,6 +189,11 @@ return async env => {
               'click',
               () => {$('#step-1-collapse').collapse('show')}
             )
+
+            document.getElementById('step-2-sign-in').addEventListener(
+              'click',
+              () => {document.getElementById('sign-in').click()}
+            )
           }
         )
       }
index b43beb1..e722cd9 100644 (file)
@@ -28,7 +28,7 @@ return async env => {
           try {
             menu = await env.site.get_menu(`${pathname.slice(0, i)}_menu.json`)
           }
-          catch (e) {
+          catch (error) {
             return pathname // fallback
           }
           let dir = pathname.slice(i, j)