Add signed-in version of my account menu allowing to change details or password
authorNick Downing <nick@ndcode.org>
Tue, 18 Jan 2022 00:05:01 +0000 (11:05 +1100)
committerNick Downing <nick@ndcode.org>
Tue, 18 Jan 2022 00:05:01 +0000 (11:05 +1100)
api/account/change_details/get.json.jst [new file with mode: 0644]
api/account/change_details/get_draft.json.jst [new file with mode: 0644]
api/account/change_details/set.json.jst [new file with mode: 0644]
api/account/change_details/set_draft.json.jst [new file with mode: 0644]
api/account/change_password.json.jst [new file with mode: 0644]
api/errors.json
my_account/index.html.jst

diff --git a/api/account/change_details/get.json.jst b/api/account/change_details/get.json.jst
new file mode 100644 (file)
index 0000000..0bde260
--- /dev/null
@@ -0,0 +1,45 @@
+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')
+
+  await post_request(
+    // env
+    env,
+    // 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)
+
+        let account = await (
+          await (
+            await transaction.get({})
+          ).get('accounts', {})
+        ).get(env.signed_in_as)
+        let details = {
+          given_names: await logjson.logjson_to_json(
+            await account.get('given_names')
+          ),
+          family_name: await logjson.logjson_to_json(
+            await account.get('family_name')
+          ),
+          contact_me: await logjson.logjson_to_json(
+            await account.get('contact_me')
+          )
+        }
+
+        await transaction.commit()
+        return details
+      }
+      catch (error) {
+        transaction.rollback()
+        throw error
+      }
+    }
+  )
+}
diff --git a/api/account/change_details/get_draft.json.jst b/api/account/change_details/get_draft.json.jst
new file mode 100644 (file)
index 0000000..cc632ec
--- /dev/null
@@ -0,0 +1,45 @@
+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')
+
+  await post_request(
+    // env
+    env,
+    // 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)
+
+        let change_details_draft = await session.get('change_details_draft')
+        let details =
+          change_details_draft !== undefined &&
+            XDate.now() < await logjson.logjson_to_json(
+              await change_details_draft.get('expires')
+            ) ? {
+              given_names: await logjson.logjson_to_json(
+                await change_details_draft.get('given_names')
+              ),
+              family_name: await logjson.logjson_to_json(
+                await change_details_draft.get('family_name')
+              ),
+              contact_me: await logjson.logjson_to_json(
+                await change_details_draft.get('contact_me')
+              )
+            } : null
+
+        await transaction.commit()
+        return details
+      }
+      catch (error) {
+        transaction.rollback()
+        throw error
+      }
+    }
+  )
+}
diff --git a/api/account/change_details/set.json.jst b/api/account/change_details/set.json.jst
new file mode 100644 (file)
index 0000000..dac0b61
--- /dev/null
@@ -0,0 +1,57 @@
+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')
+
+  await post_request(
+    // env
+    env,
+    // handler
+    async details => {
+      details = {
+        given_names: details.given_names.slice(0, 256),
+        family_name: details.family_name.slice(0, 256),
+        contact_me: details.contact_me ? true : false
+      }
+      if (details.given_names.length === 0)
+        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(env.signed_in_as)
+        account.set(
+          'given_names',
+          transaction.json_to_logjson(details.given_names)
+        )
+        account.set(
+          'family_name',
+          transaction.json_to_logjson(details.family_name)
+        )
+        account.set(
+          'contact_me',
+          transaction.json_to_logjson(details.contact_me)
+        )
+
+        await transaction.commit()
+      }
+      catch (error) {
+        transaction.rollback()
+        throw error
+      }
+    }
+  )
+}
diff --git a/api/account/change_details/set_draft.json.jst b/api/account/change_details/set_draft.json.jst
new file mode 100644 (file)
index 0000000..cf7ee6f
--- /dev/null
@@ -0,0 +1,52 @@
+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')
+
+  await post_request(
+    // env
+    env,
+    // handler
+    async details => {
+      // coerce and/or validate
+      if (details !== null)
+        details = {
+          given_names: details.given_names.slice(0, 256),
+          family_name: details.family_name.slice(0, 256),
+          contact_me: details.contact_me ? true : false
+        }
+
+      let transaction = await env.site.database.Transaction()
+      try {
+        // initialize env.session_key, set cookie in env.response
+        let session = await session_cookie(env, transaction)
+
+        if (details) {
+          let expires = new XDate()
+          expires.addDays(1)
+          session.set(
+            'change_details_draft',
+            transaction.json_to_logjson(
+              {
+                given_names: details.given_names,
+                family_name: details.family_name,
+                contact_me: details.contact_me,
+                expires: expires.getTime()
+              }
+            )
+          )
+        }
+        else
+          session.delete('change_details_draft')
+
+        await transaction.commit()
+      }
+      catch (error) {
+        transaction.rollback()
+        throw error
+      }
+    }
+  )
+}
diff --git a/api/account/change_password.json.jst b/api/account/change_password.json.jst
new file mode 100644 (file)
index 0000000..b36bb0d
--- /dev/null
@@ -0,0 +1,60 @@
+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')
+
+  await post_request(
+    // env
+    env,
+    // handler
+    async (old_password, new_password) => {
+      // coerce and/or validate
+      old_password = old_password.slice(0, 256)
+      new_password = new_password.slice(0, 256)
+      if (old_password.length < 8 || new_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(env.signed_in_as)
+
+        if (
+          old_password !== await logjson.logjson_to_json(
+            await account.get('password')
+          )
+        )
+          throw new Problem(
+            'Incorrect password',
+            `Provided old password did not match the expected value.`,
+            426
+          )
+
+        await account.set(
+          'password',
+          transaction.json_to_logjson(new_password)
+        )
+
+        await transaction.commit()
+      }
+      catch (error) {
+        transaction.rollback()
+        throw error
+      }
+    }
+  )
+}
index ae3e6dd..53b7c9f 100644 (file)
@@ -24,6 +24,7 @@
   "423": "Link code missing",
   "424": "Link code mismatch",
   "425": "Email not yet verified",
+  "426": "Incorrect password",
   "500": "Internal server error",
   "501": "Not implemented",
   "502": "Bad gateway",
index a1db88e..232f6fa 100644 (file)
@@ -1,11 +1,69 @@
-let querystring = require('querystring')
-let stream_buffers = require('stream-buffers')
+let logjson = (await import('@ndcode/logjson')).default
+let XDate = require('xdate')
 
 return async env => {
-  //let account = await _require('/account.jst')
   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 menu = await env.site.get_menu('/my_account/_menu.json')
   let navbar = await _require('/_lib/navbar.jst')
+  let session_cookie = await _require('/_lib/session_cookie.jst')
+
+  // see whether signed in, if so preload details, and draft details if any
+  let
+    transaction = await env.site.database.Transaction(),
+    details,
+    draft_details
+  try {
+    // initialize env.session_key, set cookie in env.response
+    let session = await session_cookie(env, transaction)
+    if (env.signed_in_as) {
+      account = await (
+        await (
+          await transaction.get({})
+        ).get('accounts')
+      ).get(env.signed_in_as)
+      details = {
+        given_names: await logjson.logjson_to_json(
+          await account.get('given_names')
+        ),
+        family_name: await logjson.logjson_to_json(
+          await account.get('family_name')
+        ),
+        contact_me: await logjson.logjson_to_json(
+          await account.get('contact_me')
+        )
+      }
+
+      let change_details_draft = await session.get('change_details_draft')
+      draft_details =
+        change_details_draft !== undefined &&
+          XDate.now() < await logjson.logjson_to_json(
+            await change_details_draft.get('expires')
+          ) ? {
+            given_names: await logjson.logjson_to_json(
+              await change_details_draft.get('given_names')
+            ),
+            family_name: await logjson.logjson_to_json(
+              await change_details_draft.get('family_name')
+            ),
+            contact_me: await logjson.logjson_to_json(
+              await change_details_draft.get('contact_me')
+            )
+          } : null
+    }
+    await transaction.commit()
+  }
+  catch (error) {
+    transaction.rollback()
+    throw error
+  }
+  console.log(
+    'details',
+    JSON.stringify(details),
+    'draft_details',
+    JSON.stringify(draft_details)
+  )
 
   await navbar(
     env,
@@ -15,295 +73,120 @@ return async env => {
     async _out => {
       await breadcrumbs(env, _out)
 
-      //if (Object.prototype.hasOwnProperty.call(env, 'account')) {
-      //  // signed in
-      //  div.panel.panel-default.margin-x-xl {
-      //    div.panel-heading {'Your name'}
-      //    div.panel-body {
-      //      form#change-name-form(method="post" action="index.html" role="form") {
-      //        div.row {
-      //          div.col-md-6 {
-      //            div.form-group {
-      //              label(for="change-name-form-given-names") {'Given names *'}
-      //              input.form-control#change-name-form-given-names(type="text" name="given-names" required="required" placeholder="Your given names" data-error="Given names are required." value=env.account.given_names || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //          div.col-md-6 {
-      //            div.form-group {
-      //              label(for="change-name-form-family-name") {'Family name'}
-      //              input.form-control#change-name-form-family-name(type="text" name="family-name" placeholder="Your family name" value=env.account.family_name || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        p {'Note: If your name is one word or does not fit given names/family name pattern, then please enter given names only; we will address you by your given names. Your given names will be visible to other users if you comment on our blog. Your email and family name will remain private.'}
-      //        div.row {
-      //          div.col-md-12 {
-      //            button.btn.btn-success.btn-send(type="submit") {'Change'}
-      //          }
-      //        }
-      //      }
-      //    }
-      //  }
-//
-      //  div.panel.panel-default.margin-x-xl {
-      //    div.panel-heading {'Your billing address'}
-      //    div.panel-body {
-      //      form#change-billing-address-form(method="post" action="index.html" role="form") {
-      //        div.row {
-      //          div.col-md-12 {
-      //            div.form-group {
-      //              label(for="change-billing-address-form-name") {'Name *'}
-      //              input.form-control#change-billing-address-form-name(type="text" name="billing-name" required="required" placeholder="Jane Roe" data-error="Name is required." value=env.account.billing_address.name || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-12 {
-      //            div.form-group {
-      //              label(for="change-billing-address-form-company") {'Company'}
-      //              input.form-control#change-billing-address-form-company(type="text" name="billing-company" value=env.account.billing_address.company || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-12 {
-      //            div.form-group {
-      //              label(for="change-billing-address-form-line-1") {'Address line 1 *'}
-      //              input.form-control#change-billing-address-form-line-1(type="text" name="billing-line-1" required="required" placeholder="100 Main St" data-error="Address line 1 is required." value=env.account.billing_address.line_1 || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-12 {
-      //            div.form-group {
-      //              label(for="change-billing-address-form-line-2") {'Address line 2'}
-      //              input.form-control#change-billing-address-form-line-2(type="text" name="billing-line-2" value=env.account.billing_address.line_2 || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-6 {
-      //            div.form-group {
-      //              label(for="change-billing-address-form-city") {'City *'}
-      //              input.form-control#change-billing-address-form-city(type="text" name="billing-city" placeholder="Phoenix" required="required" data-error="City is required." value=env.account.billing_address.city || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //          div.col-md-3 {
-      //            div.form-group {
-      //              label(for="change-billing-address-form-state") {'State'}
-      //              input.form-control#change-billing-address-form-state(type="text" name="billing-state" placeholder="AZ" value=env.account.billing_address.state || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //          div.col-md-3 {
-      //            div.form-group {
-      //              label(for="change-billing-address-form-postal-code") {'Postal code'}
-      //              input.form-control#change-billing-address-form-postal-code(type="text" name="billing-postal-code" placeholder="85123" value=env.account.billing_address.postal_code || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-12 {
-      //            div.form-group {
-      //              label(for="change-billing-address-form-country") {'Country *'}
-      //              input.form-control#change-billing-address-form-country(type="text" name="billing-country" required="required" placeholder="USA" data-error="Country is required." value=env.account.billing_address.country || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-12 {
-      //            div.form-group {
-      //              label(for="change-billing-address-form-telephone") {'Telephone *'}
-      //              input.form-control#change-billing-address-form-telephone(type="text" name="billing-telephone" required="required" placeholder="+1-123-456-7890" data-error="Telephone is required." value=env.account.billing_address.telephone || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-12 {
-      //            button.btn.btn-success.btn-send(type="submit") {'Change'}
-      //          }
-      //        }
-      //      }
-      //    }
-      //  }
-//
-      //  div.panel.panel-default.margin-x-xl {
-      //    div.panel-heading {'Your shipping address'}
-      //    div.panel-body {
-      //      form#change-shipping-address-form(method="post" action="index.html" role="form") {
-      //        div.row {
-      //          div.col-md-12 {
-      //            div.form-check {
-      //              input.form-check-input#sign-up-form-shipping-same-as-billing(type="checkbox" name="shipping-same-as-billing" checked="checked") {}
-      //              ' '
-      //              label(for="sign-up-form-shipping-same-as-billing") {
-      //                'Same as billing address'
-      //              }
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-12 {
-      //            div.form-group {
-      //              label(for="change-shipping-address-form-name") {'Name *'}
-      //              input.form-control#change-shipping-address-form-name(type="text" name="shipping-name" required="required" placeholder="Jane Roe" data-error="Name is required." value=env.account.shipping_address.name || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-12 {
-      //            div.form-group {
-      //              label(for="change-shipping-address-form-company") {'Company'}
-      //              input.form-control#change-shipping-address-form-company(type="text" name="shipping-company" value=env.account.shipping_address.company || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-12 {
-      //            div.form-group {
-      //              label(for="change-shipping-address-form-line-1") {'Address line 1 *'}
-      //              input.form-control#change-shipping-address-form-line-1(type="text" name="shipping-line-1" required="required" placeholder="100 Main St" data-error="Address line 1 is required." value=env.account.shipping_address.line_1 || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-12 {
-      //            div.form-group {
-      //              label(for="change-shipping-address-form-line-2") {'Address line 2'}
-      //              input.form-control#change-shipping-address-form-line-2(type="text" name="shipping-line-2" value=env.account.shipping_address.line_2 || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-6 {
-      //            div.form-group {
-      //              label(for="change-shipping-address-form-city") {'City *'}
-      //              input.form-control#change-shipping-address-form-city(type="text" name="shipping-city" placeholder="Phoenix" required="required" data-error="City is required." value=env.account.shipping_address.city || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //          div.col-md-3 {
-      //            div.form-group {
-      //              label(for="change-shipping-address-form-state") {'State'}
-      //              input.form-control#change-shipping-address-form-state(type="text" name="shipping-state" placeholder="AZ" value=env.account.shipping_address.state || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //          div.col-md-3 {
-      //            div.form-group {
-      //              label(for="change-shipping-address-form-postal-code") {'Postal code'}
-      //              input.form-control#change-shipping-address-form-postal-code(type="text" name="shipping-postal-code" placeholder="85123" value=env.account.shipping_address.postal_code || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-12 {
-      //            div.form-group {
-      //              label(for="change-shipping-address-form-country") {'Country *'}
-      //              input.form-control#change-shipping-address-form-country(type="text" name="shipping-country" required="required" placeholder="USA" data-error="Country is required." value=env.account.shipping_address.country || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-12 {
-      //            div.form-group {
-      //              label(for="change-shipping-address-form-telephone") {'Telephone *'}
-      //              input.form-control#change-shipping-address-form-telephone(type="text" name="shipping-telephone" required="required" placeholder="+1-123-456-7890" data-error="Telephone is required." value=env.account.shipping_address.telephone || "") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-12 {
-      //            button.btn.btn-success.btn-send(type="submit") {'Change'}
-      //          }
-      //        }
-      //      }
-      //    }
-      //  }
-//
-      //  div.panel.panel-default.margin-x-xl {
-      //    div.panel-heading {'Your password'}
-      //    div.panel-body {
-      //      form#change-password-form(method="post" action="index.html" role="form") {
-      //        div.row {
-      //          div.col-md-6 {
-      //            div.form-group {
-      //              label(for="change-password-form-old-password") {'Old password *'}
-      //              input.form-control#change-password-form-old-password(type="password" name="old-password" required="required" placeholder="Old password" data-error="Old password is required.") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //          div.col-md-6 {
-      //            div.form-group {
-      //              label(for="change-password-form-new-password") {'New password *'}
-      //              input.form-control#'change-password-form-new-password'(type="password" name="new-password" required="required" placeholder="New password" data-error="New password is required.") {}
-      //              div.help-block.with-errors {}
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-12 {
-      //            button.btn.btn-success.btn-send(type="submit") {'Change'}
-      //          }
-      //        }
-      //      }
-      //    }
-      //  }
-//
-      //  div.panel.panel-default.margin-x-xl {
-      //    div.panel-heading {'Your contact preference'}
-      //    div.panel-body {
-      //      form#change-contact-preference-form(method="post" action="index.html" role="form") {
-      //        div.row {
-      //          div.col-md-12 {
-      //            div.form-group {
-      //              if (env.account.contact_me)
-      //                input.form-check-input#change-contact-preference-form-contact-me(type="checkbox" name="contact-me" checked="checked") {}
-      //              else
-      //                input.form-check-input#change-contact-preference-form-contact-me(type="checkbox" name="contact-me") {}
-      //              ' '
-      //              label(for="change-contact-preference-form-contact-me") {
-      //                'Contact me by email with updates and special offers'
-      //              }
-      //            }
-      //          }
-      //        }
-      //        div.row {
-      //          div.col-md-12 {
-      //            button.btn.btn-success.btn-send(type="submit") {'Change'}
-      //          }
-      //        }
-      //      }
-      //    }
-      //  }
-//
-      //  div.row {
-      //    div.col-md-12 {
-      //      p.text-muted {
-      //        strong {'*'}
-      //        'These fields are required.'
-      //      }
-      //    }
-      //  }
-      //}
-      //else {
+      if (env.signed_in_as) {
+        // signed in
+        p {'Your given names are visible to other users if you comment on our blog. Your email and family name remain private. If your name is one word or does not fit given names/family name pattern, then please enter given names only.'}
+
+        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") {
+                'Change 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="given-names") {'Given names *'}
+                      input.form-control#given-names(type="text" value=draft_details ? draft_details.given_names : details.given_names placeholder="Your given names" required="required" maxlength=256) {}
+                    }
+                  }
+                  div.col-md-6 {
+                    div.form-group {
+                      label.form-label(for="family-name") {'Family name'}
+                      input.form-control#family-name(type="text" value=draft_details ? draft_details.family_name : details.family_name placeholder="Your family name" maxlength=256) {}
+                    }
+                  }
+                }
+                div.row.mb-3 {
+                  div.col-md-12 {
+                    div.custom-control.custom-checkbox {
+                      if (draft_details ? draft_details.contact_me : details.contact_me)
+                        input.custom-control-input#contact-me(type="checkbox" checked="checked") {}
+                      else
+                        input.custom-control-input#contact-me(type="checkbox") {}
+                      ' '
+                      label.custom-control-label(for="contact-me") {
+                        'Contact me by email with updates and special offers'
+                      }
+                    }
+                  }
+                }
+
+                if (draft_details)
+                  button.btn.btn-outline-secondary#step-1-revert(type="button") {'Revert'}
+                else
+                  button.btn.btn-outline-secondary#step-1-revert(type="button" disabled="disabled") {'Revert'}
+                if (draft_details)
+                  button.btn.btn-success.ml-3#step-1-save(type="button") {'Save'}
+                else
+                  button.btn.btn-success.ml-3#step-1-save(type="button" disabled="disabled") {'Save'}
+
+                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") {
+                'Change password'
+              }
+            }
+            div#step-2-collapse.collapse(role="tabpanel" aria-labelledby="step-2-heading" data-parent="#accordion") {
+              div.card-body {
+                div.row.mb-3 {
+                  div.col-md-6 {
+                    div.form-group {
+                      label.form-label(for="old-password") {'Old password *'}
+                      input.form-control#old-password(type="password" placeholder="Old password" required="required" minlength=8 maxlength=256) {}
+                    }
+                  }
+                  div.col-md-6 {
+                    div.form-group {
+                      label.form-label(for="new-password") {'New password *'}
+                      input.form-control#'new-password'(type="password" placeholder="New password" required="required" minlength=8 maxlength=256) {}
+                    }
+                  }
+                }
+
+                button.btn.btn-outline-secondary#step-2-clear(type="button" disabled="disabled") {'Clear'}
+                button.btn.btn-success.ml-3#step-2-save(type="button" disabled="disabled") {'Save'}
+
+                p.'mt-3'.mb-0 {'* These fields are required.'}
+              }
+            }
+          }
+        }
+      }
+      else {
         // signed out
         p {'For account maintenance, please click on one of the options below.'}
 
@@ -328,7 +211,7 @@ return async env => {
               }
           }
         }
-      //}
+      }
     },
     // scripts
     async _out => {
@@ -337,164 +220,264 @@ return async env => {
         function sign_in_out(status) {
           window.location.reload()
         }
+      }
 
-        $(document).ready(
-          () => {
-            // when change name form is submitted, do not reload the page
-            $(document).on(
-              'submit',
-              '#change-name-form',
-              e => {
-                e.preventDefault()
-                $.ajax(
-                  {
-                    url: 'index.html',
-                    type: 'POST',
-                    data: {
-                      what: 'name',
-                      'given-names': $('#change-name-form-given-names').val(),
-                      'family-name': $('#change-name-form-family-name').val()
-                     },
-                    success: (data, textStatus, jqXHR) => {
-                      $('#message-modal-message').text(data)
-                      $('#message-modal').modal('show')
-                    },
-                    error: (jqXHR, textStatus, errorThrown) => {
-                      $('#message-modal-message').text(errorThrown)
-                      $('#message-modal').modal('show')
-                    }
-                  }
-                )
-              }
+      if (env.signed_in_as) {
+        //script(src="/js/api_call.js") {}
+
+        script {
+          let api_account_change_details_get = async (...arguments) => api_call(
+            '/api/account/change_details/get.json',
+            ...arguments
+          )
+          let api_account_change_details_set = async (...arguments) => api_call(
+            '/api/account/change_details/set.json',
+            ...arguments
+          )
+          //let api_account_change_details_get_draft = async (...arguments) => api_call(
+          //  '/api/account/change_details/get_draft.json',
+          //  ...arguments
+          //)
+          let api_account_change_details_set_draft = async (...arguments) => api_call(
+            '/api/account/change_details/set_draft.json',
+            ...arguments
+          )
+          let api_account_change_password = async (...arguments) => api_call(
+            '/api/account/change_password.json',
+            ...arguments
+          )
+
+          let step_1_dirty = ${JSON.stringify(draft_details !== null)}
+          let draft_timeout_running = false
+          let draft_timeout_handler = async () => {
+            draft_timeout_running = false
+            await api_account_change_details_set_draft(
+              step_1_dirty ? {
+                given_names: document.getElementById('given-names').value.slice(0, 256),
+                family_name: document.getElementById('family-name').value.slice(0, 256),
+                contact_me: document.getElementById('contact-me').checked ? true : false
+              } : null
             )
+            //console.log('draft', await api_account_change_details_get_draft())
+          }
 
-            // when change billing address form is submitted, do not reload the page
-            $(document).on(
-              'submit',
-              '#change-billing-address-form',
-              e => {
-                e.preventDefault()
-                $.ajax(
-                  {
-                    url: 'index.html',
-                    type: 'POST',
-                    data: {
-                      what: 'billing-address',
-                      name: $('#change-billing-address-form-name').val(),
-                      company: $('#change-billing-address-form-company').val(),
-                      'line-1': $('#change-billing-address-form-line-1').val(),
-                      'line-2': $('#change-billing-address-form-line-2').val(),
-                      city: $('#change-billing-address-form-city').val(),
-                      state: $('#change-billing-address-form-state').val(),
-                      'postal-code': $('#change-billing-address-form-postal-code').val(),
-                      country: $('#change-billing-address-form-country').val(),
-                      telephone: $('#change-billing-address-form-telephone').val()
-                     },
-                    success: (data, textStatus, jqXHR) => {
-                      $('#message-modal-message').text(data)
-                      $('#message-modal').modal('show')
-                    },
-                    error: (jqXHR, textStatus, errorThrown) => {
-                      $('#message-modal-message').text(errorThrown)
-                      $('#message-modal').modal('show')
-                    }
-                  }
-                )
+          document.addEventListener(
+            'DOMContentLoaded',
+            () => {
+              let step_1_change_handler = () => {
+                step_1_dirty = true
+                document.getElementById('step-1-revert').disabled = false
+                document.getElementById('step-1-save').disabled = false
+    
+                if (!draft_timeout_running) {
+                  draft_timeout_running = true
+                  setTimeout(draft_timeout_handler, 5000)
+                }
               }
-            )
+              document.getElementById('given-names').addEventListener(
+                'change',
+                step_1_change_handler
+              )
+              document.getElementById('family-name').addEventListener(
+                'change',
+                step_1_change_handler
+              )
+              document.getElementById('contact-me').addEventListener(
+                'change',
+                step_1_change_handler
+              )
 
-            // when change shipping address form is submitted, do not reload the page
-            $(document).on(
-              'submit',
-              '#change-shipping-address-form',
-              e => {
-                e.preventDefault()
-                $.ajax(
-                  {
-                    url: 'index.html',
-                    type: 'POST',
-                    data: {
-                      what: 'shipping-address',
-                      name: $('#change-shipping-address-form-name').val(),
-                      company: $('#change-shipping-address-form-company').val(),
-                      'line-1': $('#change-shipping-address-form-line-1').val(),
-                      'line-2': $('#change-shipping-address-form-line-2').val(),
-                      city: $('#change-shipping-address-form-city').val(),
-                      state: $('#change-shipping-address-form-state').val(),
-                      'postal-code': $('#change-shipping-address-form-postal-code').val(),
-                      country: $('#change-shipping-address-form-country').val(),
-                      telephone: $('#change-shipping-address-form-telephone').val()
-                     },
-                    success: (data, textStatus, jqXHR) => {
-                      $('#message-modal-message').text(data)
-                      $('#message-modal').modal('show')
-                    },
-                    error: (jqXHR, textStatus, errorThrown) => {
-                      $('#message-modal-message').text(errorThrown)
-                      $('#message-modal').modal('show')
-                    }
+              document.getElementById('step-1-revert').addEventListener(
+                'click',
+                async () => {
+                  $('#step-1-tick').hide()
+                  $('#step-1-cross').hide()
+                  $('#step-1-spinner').show()
+
+                  let details      
+                  try {
+                    details = await api_account_change_details_get()
                   }
-                )
-              }
-            )
+                  catch (error) {
+                    let problem =
+                      error instanceof Problem ?
+                        error :
+                        new Problem(
+                          // title
+                          'Bad request',
+                          // detail
+                          (error.stack || error.message).toString()
+                          // status
+                          400
+                        )
+                    console.log(problem.detail)
+      
+                    $('#step-1-tick').hide()
+                    $('#step-1-cross').show()
+                    $('#step-1-spinner').hide()
+                    return
+                  }
+                  $('#step-1-tick').hide()
+                  $('#step-1-cross').hide()
+                  $('#step-1-spinner').hide()
 
-            // when change password form is submitted, do not reload the page
-            $(document).on(
-              'submit',
-              '#change-password-form',
-              e => {
-                e.preventDefault()
-                $.ajax(
-                  {
-                    url: 'index.html',
-                    type: 'POST',
-                    data: {
-                      what: 'password',
-                      'old-password': $('#change-password-form-old-password').val(),
-                      'new-password': $('#change-password-form-new-password').val(),
-                     },
-                    success: (data, textStatus, jqXHR) => {
-                      $('#message-modal-message').text(data)
-                      $('#message-modal').modal('show')
-                    },
-                    error: (jqXHR, textStatus, errorThrown) => {
-                      $('#message-modal-message').text(errorThrown)
-                      $('#message-modal').modal('show')
-                    }
+                  step_1_dirty = false
+                  document.getElementById('step-1-revert').disabled = true
+                  document.getElementById('step-1-save').disabled = true
+
+                  document.getElementById('given-names').value = details.given_names
+                  document.getElementById('family-name').value = details.family_name
+                  document.getElementById('contact-me').checked = details.contact_me
+
+                  if (!draft_timeout_running) {
+                    draft_timeout_running = true
+                    setTimeout(draft_timeout_handler, 5000)
                   }
-                )
-              }
-            )
+                }
+              )
 
-            //return
-            // when change contact preference form is submitted, do not reload the page
-            $(document).on(
-              'submit',
-              '#change-contact-preference-form',
-              e => {
-                e.preventDefault()
-                $.ajax(
-                  {
-                    url: 'index.html',
-                    type: 'POST',
-                    data: {
-                      what: 'contact-preference',
-                      'contact-me': $('#change-contact-preference-form-contact-me').prop('checked')
-                     },
-                    success: (data, textStatus, jqXHR) => {
-                      $('#message-modal-message').text(data)
-                      $('#message-modal').modal('show')
-                    },
-                    error: (jqXHR, textStatus, errorThrown) => {
-                      $('#message-modal-message').text(errorThrown)
-                      $('#message-modal').modal('show')
-                    }
+              document.getElementById('step-1-save').addEventListener(
+                'click',
+                async () => {
+                  if (
+                    !document.getElementById('given-names').reportValidity() ||
+                      !document.getElementById('family-name').reportValidity()
+                  ) {
+                    $('#step-1-tick').hide()
+                    $('#step-1-cross').show()
+                    $('#step-1-spinner').hide()
+                    return false
                   }
-                )
+                  $('#step-1-tick').hide()
+                  $('#step-1-cross').hide()
+                  $('#step-1-spinner').show()
+      
+                  try {
+                    await api_account_change_details_set(
+                      {
+                        given_names: document.getElementById('given-names').value.slice(0, 256),
+                        family_name: document.getElementById('family-name').value.slice(0, 256),
+                        contact_me: document.getElementById('contact-me').checked ? true : false
+                      }
+                    )
+                  }
+                  catch (error) {
+                    let problem =
+                      error instanceof Problem ?
+                        error :
+                        new Problem(
+                          // title
+                          'Bad request',
+                          // detail
+                          (error.stack || error.message).toString()
+                          // status
+                          400
+                        )
+                    console.log(problem.detail)
+      
+                    $('#step-1-tick').hide()
+                    $('#step-1-cross').show()
+                    $('#step-1-spinner').hide()
+                    return
+                  }
+                  $('#step-1-tick').show()
+                  $('#step-1-cross').hide()
+                  $('#step-1-spinner').hide()
+
+                  step_1_dirty = false
+                  document.getElementById('step-1-revert').disabled = true
+                  document.getElementById('step-1-save').disabled = true
+
+                  // SHOULD execute immediately here
+                  // (because user is likely to leave the page after save)
+                  if (!draft_timeout_running) {
+                    draft_timeout_running = true
+                    setTimeout(draft_timeout_handler, 5000)
+                  }
+                }
+              )
+
+              let step_2_change_handler = () => {
+                document.getElementById('step-2-clear').disabled = false
+                document.getElementById('step-2-save').disabled = false
               }
-            )
-          }
-        )
+              document.getElementById('old-password').addEventListener(
+                'change',
+                step_2_change_handler
+              )
+              document.getElementById('new-password').addEventListener(
+                'change',
+                step_2_change_handler
+              )
+
+              document.getElementById('step-2-clear').addEventListener(
+                'click',
+                () => {
+                  document.getElementById('step-2-clear').disabled = true
+                  document.getElementById('step-2-save').disabled = true
+
+                  document.getElementById('old-password').value = ''
+                  document.getElementById('new-password').value = ''
+                }
+              )
+
+              document.getElementById('step-2-save').addEventListener(
+                'click',
+                async () => {
+                  if (
+                    !document.getElementById('old-password').reportValidity() ||
+                      !document.getElementById('new-password').reportValidity()
+                  ) {
+                    $('#step-2-tick').hide()
+                    $('#step-2-cross').show()
+                    $('#step-2-spinner').hide()
+                    return false
+                  }
+                  $('#step-2-tick').hide()
+                  $('#step-2-cross').hide()
+                  $('#step-2-spinner').show()
+      
+                  try {
+                    await api_account_change_password(
+                      // old_password
+                      document.getElementById('old-password').value.slice(0, 256),
+                      // new_password
+                      document.getElementById('new-password').value.slice(0, 256)
+                    )
+                  }
+                  catch (error) {
+                    let problem =
+                      error instanceof Problem ?
+                        error :
+                        new Problem(
+                          // title
+                          'Bad request',
+                          // detail
+                          (error.stack || error.message).toString()
+                          // status
+                          400
+                        )
+                    console.log(problem.detail)
+      
+                    $('#step-2-tick').hide()
+                    $('#step-2-cross').show()
+                    $('#step-2-spinner').hide()
+                    return
+                  }
+                  $('#step-2-tick').show()
+                  $('#step-2-cross').hide()
+                  $('#step-2-spinner').hide()
+
+                  document.getElementById('step-2-clear').disabled = true
+                  document.getElementById('step-2-save').disabled = true
+
+                  document.getElementById('old-password').value = ''
+                  document.getElementById('new-password').value = ''
+                }
+              )
+            }
+          )
+        }
       }
     }
   )