Add /api/sign_(in|out).json endpoints, uncomment sign in/out logic in navbar and...
authorNick Downing <nick@ndcode.org>
Sat, 15 Jan 2022 06:27:23 +0000 (17:27 +1100)
committerNick Downing <nick@ndcode.org>
Sat, 15 Jan 2022 06:27:23 +0000 (17:27 +1100)
12 files changed:
_lib/navbar.jst
_lib/post_request.jst
api/errors.json
api/feedback.html.jst [moved from feedback.html.jst with 92% similarity]
api/sign_in.json.jst [new file with mode: 0644]
api/sign_out.json.jst [new file with mode: 0644]
api/sign_up/create_account.json.jst
api/sign_up/get_draft.json.jst
api/sign_up/send_verification_email.json.jst
api/sign_up/set_draft.json.jst
api/sign_up/verify_email.json.jst
css/bootstrap/_variables.scss

index bf97f48..c6ff55f 100644 (file)
@@ -57,17 +57,17 @@ return async (env, head, body, scripts) => {
                 }
                 ' '
                 if (env.signed_in_as !== null)
-                  a#sign-in(href="" hidden="") {'Sign in'}
+                  a#sign-in(href="#" style="display: none;") {'Sign in'}
                 else
-                  a#sign-in(href="") {'Sign in'}
+                  a#sign-in(href="#") {'Sign in'}
                 ' '
                 if (env.signed_in_as !== null)
-                  a#sign-out(href="") {'Sign out'}
+                  a#sign-out(href="#") {'Sign out'}
                 else
-                  a#sign-out(href="" hidden="") {'Sign out'}
+                  a#sign-out(href="#" style="display: none;") {'Sign out'}
               }
   
-              form/*.form-inline*/(action="/search/index.html") {
+              form(action="/search/index.html") {
                 div.input-group {
                   input.form-control(name="query" type="text" placeholder="Search" aria-describedby="search-button") {}
                   div.input-group-append {
@@ -159,7 +159,7 @@ return async (env, head, body, scripts) => {
             }
             ul.navbar-nav.ml-auto {
               li.nav-item {
-                a.nav-link#give-feedback(href="") {'Give feedback'}
+                a.nav-link#give-feedback(href="#") {'Give feedback'}
               }
             }
           }
@@ -194,56 +194,54 @@ return async (env, head, body, scripts) => {
       }
 
       // hidden part
-      //div#sign-in-modal.modal.fade(role="dialog") {
-      //  div.modal-dialog {
-      //    div.modal-content {
-      //      div.modal-header {
-      //        span.h4.modal-title {'Sign in'}
-      //      }
-      //      div.modal-body {
-      //        form#sign-in-form(method="post" action="/sign_in.json" role="form") {
-      //          div.row {
-      //            div.col-md-12 {
-      //              div.form-group {
-      //                label(for="sign-in-form-email") {'Email'}
-      //                input.form-control#sign-in-form-email(type="text" name="email" placeholder="Please enter your email address" required="required" data-error="Email address is required.") {}
-      //                div.help-block.with-errors {}
-      //              }
-      //            }
-      //          }
-      //          div.row {
-      //            div.col-md-12 {
-      //              div.form-group {
-      //                label(for="sign-in-form-password") {'Password'}
-      //                input.form-control#sign-in-form-password(type="password" name="password" required="required" placeholder="Please enter your password" data-error="Password is required.") {}
-      //                div.help-block.with-errors {}
-      //              }
-      //            }
-      //          }
-      //          input.btn.btn-success.btn-send(style="display: none;" type="submit" value="Sign in") {}
-      //        }
+      div#sign-in-modal.modal.fade(role="dialog") {
+        div.modal-dialog {
+          div.modal-content {
+            div.modal-header {
+              span.h4.modal-title {'Sign in'}
+            }
+            div.modal-body {
+              form#sign-in-form {
+                div.row {
+                  div.col-md-12 {
+                    div.form-group {
+                      label.form-label(for="sign-in-form-email") {'Email'}
+                      input.form-control#sign-in-form-email(type="text" name="email" placeholder="Account email address" required="required" maxlength=256) {}
+                    }
+                  }
+                }
+                div.row {
+                  div.col-md-12 {
+                    div.form-group {
+                      label.form-label(for="sign-in-form-password") {'Password'}
+                      input.form-control#sign-in-form-password(type="password" name="password" placeholder="Account password" required="required" minlength=8 maxlength=256) {}
+                    }
+                  }
+                }
+                input.btn.btn-success.btn-send(style="display: none;" type="submit" value="Sign in") {}
+              }
 
-      //        p {
-      //          'No account yet? '
-      //          a(href="/my_account/sign_up/index.html") {'Sign up'}
-      //        }
+              p.mt-2 {
+                'No account yet? '
+                a(href="/my_account/sign_up/index.html") {'Sign up'}
+              }
 
-      //        p {
-      //          'Forgot password? '
-      //          a(href="/my_account/password_reset/index.html") {'Password reset'}
-      //        }
-      //      }
-      //      div.modal-footer {
-      //        button.btn.btn-primary(type="submit" form="sign-in-form") {
-      //          'Sign in'
-      //        }
-      //        button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
-      //          'Cancel'
-      //        }
-      //      }
-      //    }
-      //  }
-      //}
+              p {
+                'Forgot password? '
+                a(href="/my_account/password_reset/index.html") {'Password reset'}
+              }
+            }
+            div.modal-footer {
+              button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
+                'Cancel'
+              }
+              button.btn.btn-primary(type="submit" form="sign-in-form") {
+                'Sign in'
+              }
+            }
+          }
+        }
+      }
 
       div#feedback-modal.modal.fade(role="dialog") {
         div.modal-dialog {
@@ -255,7 +253,7 @@ return async (env, head, body, scripts) => {
               p {
                 'Did you notice something not quite right, or just want to share your impression of this page?'
               }
-              form#feedback-form(method="post" action="/feedback.html" role="form") {
+              form#feedback-form {
                 div.row {
                   div.col-md-12 {
                     div.form-group {
@@ -281,12 +279,12 @@ return async (env, head, body, scripts) => {
               }
             }
             div.modal-footer {
-              button.btn.btn-primary(type="submit" form="feedback-form") {
-                'Submit'
-              }
               button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
                 'Cancel'
               }
+              button.btn.btn-primary(type="submit" form="feedback-form") {
+                'Submit'
+              }
             }
           }
         }
@@ -298,8 +296,7 @@ return async (env, head, body, scripts) => {
             div.modal-header {
               span.h4.modal-title {'Message'}
             }
-            div.modal-body {
-              p#message-modal-message {}
+            div.modal-body#message-modal-message {
             }
             div.modal-footer {
               button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
@@ -312,125 +309,116 @@ return async (env, head, body, scripts) => {
     },
     // scripts
     async _out => {
-      //script(src="/js/sha256.js") {}
+      script(src="/js/api_call.js") {}
 
       script {
-        //function get_cookie(name) {
-        //  let entries = document.cookie.split(';');
-        //  for (let i = 0; i < entries.length; ++i) {
-        //    let fields = entries[i].split('=');
-        //    if (fields[0].trim() === name)
-        //      return decodeURIComponent(fields[1]);
-        //  }
-        //  return undefined;
-        //}
+        let sign_in = async (...arguments) => api_call(
+          '/api/sign_in.json',
+          ...arguments
+        )
+        let sign_out = async (...arguments) => api_call(
+          '/api/sign_out.json',
+          ...arguments
+        )
 
-        //// this function can be overridden in a further script
-        //function sign_in_out(status) {
-        //}
+        // this function can be overridden in a further script
+        function sign_in_out(status) {
+        }
 
-        $(document).ready(
+        document.addEventListener(
+          'DOMContentLoaded',
           () => {
-            //// sign in form
-            //$('#sign-in').click(
-            //  () => {
-            //    $('#sign-in-form-email').text('')
-            //    $('#sign-in-form-password').text('')
-            //    $('#sign-in-modal').modal('show')
-            //    return false
-            //  }
-            //)
-            //$('#sign-in-modal').on(
-            //  'shown.bs.modal',
-            //  () => {
-            //    $('#sign-in-form-email').focus()
-            //  }
-            //)
-            //// when sign in form is submitted, do not reload the page
-            //$(document).on(
-            //  'submit',
-            //  '#sign-in-form',
-            //  e => {
-            //    e.preventDefault()
-            //    $.ajax(
-            //      {
-            //        url: '/my_account/sign_in.json',
-            //        type: 'POST',
-            //        data: {
-            //          email: $('#sign-in-form-email').val(),
-            //          password: sha256(
-            //            get_cookie('session_key') +
-            //            $('#sign-in-form-password').val()
-            //          ).toString('hex')
-            //        },
-            //        success: (data, textStatus, jqXHR) => {
-            //          $('#sign-in-modal').modal('hide')
-            //          switch (data.result) {
-            //          case 1: // success
-            //            $('#signed-in-status').text(data.signed_in_status)
-            //            $('#sign-in').hide()
-            //            $('#sign-out').show()
-            //            sign_in_out(true) // notify navbar caller
-            //            break
-            //          case 2: // redirect
-            //            location.href = data.redirect_href
-            //            break
-            //          }
-            //          $('#message-modal-message').text(data.message)
-            //          $('#message-modal').modal('show')
-            //        },
-            //        error: (jqXHR, textStatus, errorThrown) => {
-            //          $('#sign-in-modal').modal('hide')
-            //          $('#message-modal-message').text(errorThrown)
-            //          $('#message-modal').modal('show')
-            //        }
-            //      }
-            //    )
-            //  }
-            //)
+            // sign in form
+            document.getElementById('sign-in').addEventListener(
+              'click',
+              () => {
+                document.getElementById('sign-in-form-email').value = ''
+                document.getElementById('sign-in-form-password').value = ''
+                $('#sign-in-modal').modal('show')
+              }
+            )
 
-            //// sign out button
-            //$('#sign-out').click(
-            //  () => {
-            //    $.ajax(
-            //      {
-            //        url: '/my_account/sign_out.json',
-            //        type: 'GET',
-            //        success: (data, textStatus, jqXHR) => {
-            //          if (data.result) {
-            //            $('#signed-in-status').text(data.signed_in_status)
-            //            $('#sign-in').show()
-            //            $('#sign-out').hide()
-            //            sign_in_out(false) // notify navbar caller
-            //          }
-            //          $('#message-modal-message').text(data.message)
-            //          $('#message-modal').modal('show')
-            //        },
-            //        error: (jqXHR, textStatus, errorThrown) => {
-            //          $('#message-modal-message').text(errorThrown)
-            //          $('#message-modal').modal('show')
-            //        }
-            //      }
-            //    )
-            //    return false
-            //  }
-            //)
+            $('#sign-in-modal').on(
+              'shown.bs.modal',
+              () => {
+                console.log('bloo')
+                $('#sign-in-form-email').focus()
+              }
+            )
+
+            $(document).on(
+              'submit',
+              '#sign-in-form',
+              async e => {
+                e.preventDefault()
+                let email
+                try {
+                  email = document.getElementById('sign-in-form-email').value.slice(0, 256).toLowerCase()
+                  await sign_in(
+                    email,
+                    document.getElementById('sign-in-form-password').value.slice(0, 256)
+                  )
+                }
+                catch (error) {
+                  document.getElementById('message-modal-message').textContent = error.message
+                  $('#sign-in-modal').modal('hide')
+                  $('#message-modal').modal('show')
+                  return
+                }
+
+                document.getElementById('signed-in-status').textContent = `Signed in as ${email}.`
+                $('#sign-in').hide()
+                $('#sign-out').show()
+                sign_in_out(true)
+
+                document.getElementById('message-modal-message').textContent = `You are now signed in as "${email}".`
+                $('#sign-in-modal').modal('hide')
+                $('#message-modal').modal('show')
+              }
+            )
+
+            // sign out button
+            document.getElementById('sign-out').addEventListener(
+              'click',
+              async () => {
+                try {
+                  await sign_out()
+                }
+                catch (error) {
+                  document.getElementById('message-modal-message').textContent = error.message
+                  $('#sign-in-modal').modal('hide')
+                  $('#message-modal').modal('show')
+                  return
+                }
+
+                document.getElementById('signed-in-status').textContent = 'Browsing as guest.'
+                $('#sign-in').show()
+                $('#sign-out').hide()
+                sign_in_out(false)
+
+                document.getElementById('message-modal-message').textContent = `You are now signed out.`
+                $('#sign-in-modal').modal('hide')
+                $('#message-modal').modal('show')
+              }
+            )
 
             // feedback form
-            $('#give-feedback').click(
+            document.getElementById('give-feedback').addEventListener(
+              'click',
               () => {
                 $('#feedback-form-message').text('')
                 $('#feedback-modal').modal('show')
                 return false
               }
             )
+
             $('#feedback-modal').on(
               'shown.bs.modal',
               () => {
                 $('#feedback-form-message').focus()
               }
             )
-            // when feedback form is submitted, do not reload the page
+
             $(document).on(
               'submit',
               '#feedback-form',
@@ -438,7 +426,7 @@ return async (env, head, body, scripts) => {
                 e.preventDefault()
                 $.ajax(
                   {
-                    url: '/feedback.html',
+                    url: '/api/feedback.html',
                     type: 'POST',
                     data: {
                       page: window.location.href,
@@ -446,12 +434,13 @@ return async (env, head, body, scripts) => {
                     },
                     success: (data, textStatus, jqXHR) => {
                       $('#feedback-modal').modal('hide')
+                      document.getElementById('message-modal-message').textContent = data
                       $('#message-modal-message').text(data)
                       $('#message-modal').modal('show')
                     },
                     error: (jqXHR, textStatus, errorThrown) => {
                       $('#feedback-modal').modal('hide')
-                      $('#message-modal-message').text(errorThrown)
+                      document.getElementById('message-modal-message').textContent = errorThrown
                       $('#message-modal').modal('show')
                     }
                   }
index 2b968bb..59c780e 100644 (file)
@@ -1,6 +1,6 @@
 let stream_buffers = require('stream-buffers')
 
-return async (env, endpoint, func) => {
+return async (env, endpoint, handler) => {
   let Problem = await _require('/_lib/Problem.jst')
 
   let result
@@ -26,7 +26,7 @@ return async (env, endpoint, func) => {
     let arguments = JSON.parse((await data).toString())
     console.log('endpoint', endpoint, 'arguments', JSON.stringify(arguments))
 
-    result = await func(...arguments)
+    result = await handler(...arguments)
     if (result === undefined)
       result = null
     console.log('endpoint', endpoint, 'result', JSON.stringify(result))
index 4ac84de..ae3e6dd 100644 (file)
@@ -23,6 +23,7 @@
   "422": "Email already verified",
   "423": "Link code missing",
   "424": "Link code mismatch",
+  "425": "Email not yet verified",
   "500": "Internal server error",
   "501": "Not implemented",
   "502": "Bad gateway",
similarity index 92%
rename from feedback.html.jst
rename to api/feedback.html.jst
index b7b68e6..b67836b 100644 (file)
@@ -4,6 +4,9 @@ let XDate = require('xdate')
 
 return async env => {
   let globals = await env.site.get_json('/_config/globals.json')
+  let nodemailer_feedback = await env.site.get_nodemailer(
+    '/_config/email_feedback.json'
+  )
 
   let message
   if (env.request.method === 'POST') {
@@ -37,10 +40,7 @@ return async env => {
     transaction.commit()
 
     // send email (asynchronously)
-    let emailjs_feedback = await env.site.get_emailjs(
-      '/_config/email_feedback.json'
-    )
-    emailjs_feedback.send(
+    nodemailer_feedback.sendMail(
       {
         from: globals.feedback_from,
         to: globals.feedback_to,
diff --git a/api/sign_in.json.jst b/api/sign_in.json.jst
new file mode 100644 (file)
index 0000000..30f8564
--- /dev/null
@@ -0,0 +1,72 @@
+let logjson = (await import('@ndcode/logjson')).default
+
+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/sign_in.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
+        let session = await session_cookie(env, transaction)
+
+        let account = await (
+          await (
+            await transaction.get({})
+          ).get('accounts', {})
+        ).get(email)
+        if (
+          account === undefined ||
+            password !== await logjson.logjson_to_json(
+              await account.get('password')
+            )
+        )
+          throw new Problem(
+            'Unauthorized',
+            'Email and password combination was incorrect.'
+            401
+          )
+
+        if (
+          !await logjson.logjson_to_json(
+            await account.get('email_verified')
+          )
+        )
+          throw new Problem(
+            'Email not yet verified',
+            'Please verify your email address via email link before trying to sign in.',
+            425
+          )
+
+        session.set('signed_in_as', transaction.json_to_logjson(email))
+
+        await transaction.commit()
+      }
+      catch (error) {
+        transaction.rollback()
+        throw error
+      }
+    }
+  )
+}
diff --git a/api/sign_out.json.jst b/api/sign_out.json.jst
new file mode 100644 (file)
index 0000000..768de0e
--- /dev/null
@@ -0,0 +1,33 @@
+let logjson = (await import('@ndcode/logjson')).default
+
+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/sign_out.json',
+    // handler
+    async () => {
+      let transaction = await env.site.database.Transaction()
+      try {
+        // initialize env.session_key, set cookie in env.response
+        let session = await session_cookie(env, transaction)
+
+        session.set('signed_in_as', null)
+        await transaction.commit()
+      }
+      catch (error) {
+        transaction.rollback()
+        throw error
+      }
+    }
+  )
+}
index c3c74b6..5789c6a 100644 (file)
@@ -9,8 +9,8 @@ return async env => {
     // env
     env,
     // endpoint
-    '/api/sign_up/create_account',
-    // func
+    '/api/sign_up/create_account.json',
+    // handler
     async (verification_code, details) => {
       // coerce and/or validate
       verification_code = verification_code.slice(0, 6).toLowerCase()
index 6cdd586..6bdb55a 100644 (file)
@@ -10,8 +10,8 @@ return async env => {
     // env
     env,
     // endpoint
-    '/api/sign_up/get_draft',
-    // func
+    '/api/sign_up/get_draft.json',
+    // handler
     async () => {
       let transaction = await env.site.database.Transaction()
       try {
index 7ef9d16..3ab682c 100644 (file)
@@ -3,10 +3,10 @@ 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 globals = await env.site.get_json('/_config/globals.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')
@@ -15,8 +15,8 @@ return async env => {
     // env
     env,
     // endpoint
-    '/api/sign_up/send_verification_email',
-    // func
+    '/api/sign_up/send_verification_email.json',
+    // handler
     async email => {
       // coerce and/or validate
       email = email.slice(0, 256).toLowerCase()
index de69d3a..3b3c71b 100644 (file)
@@ -9,8 +9,8 @@ return async env => {
     // env
     env,
     // endpoint
-    '/api/sign_up/set_draft',
-    // func
+    '/api/sign_up/set_draft.json',
+    // handler
     async details => {
       // coerce and/or validate
       details = {
index b82e328..dc70f77 100644 (file)
@@ -11,8 +11,8 @@ return async env => {
     // env
     env,
     // endpoint
-    '/api/sign_up/verify_email',
-    // func
+    '/api/sign_up/verify_email.json',
+    // handler
     async (email, link_code) => {
       // coerce and/or validate
       email = email.slice(0, 256).toLowerCase()
index 7b81e0f..5b37add 100644 (file)
@@ -965,7 +965,7 @@ $modal-content-color:               null !default;
 $modal-content-bg:                  $white !default;
 $modal-content-border-color:        rgba($black, .2) !default;
 $modal-content-border-width:        $border-width !default;
-$modal-content-border-radius:       $border-radius-lg !default;
+$modal-content-border-radius:       0 !default; //$border-radius-lg !default;
 $modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default;
 $modal-content-box-shadow-xs:       0 .25rem .5rem rgba($black, .5) !default;
 $modal-content-box-shadow-sm-up:    0 .5rem 1rem rgba($black, .5) !default;
@@ -976,7 +976,7 @@ $modal-header-border-color:         $border-color !default;
 $modal-footer-border-color:         $modal-header-border-color !default;
 $modal-header-border-width:         $modal-content-border-width !default;
 $modal-footer-border-width:         $modal-header-border-width !default;
-$modal-header-padding-y:            1rem !default;
+$modal-header-padding-y:            0.5rem !default; //1rem !default;
 $modal-header-padding-x:            1rem !default;
 $modal-header-padding:              $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility