New approach to sign up form
authorNick Downing <nick@ndcode.org>
Mon, 10 Jan 2022 05:13:25 +0000 (16:13 +1100)
committerNick Downing <nick@ndcode.org>
Mon, 10 Jan 2022 05:13:25 +0000 (16:13 +1100)
16 files changed:
_menu.json
_svg/icon_password_reset.svg [new file with mode: 0644]
_svg/icon_sign_up.svg [new file with mode: 0644]
api/errors.json [new file with mode: 0644]
api/sign_up.js.min [new file with mode: 0644]
api/sign_up.json.jst [new file with mode: 0644]
api/verification_image.png.jst [new file with mode: 0644]
css/bootstrap/custom.less
css/bootstrap/modals.less
css/bootstrap/variables.less
my_account/_menu.json [new file with mode: 0644]
my_account/index.html.jst [new file with mode: 0644]
my_account/sign_up/index.html.jst [new file with mode: 0644]
package.json
page.jst
session_cookie.jst [new file with mode: 0644]

index 1fa726e..9a1b5bf 100644 (file)
@@ -1,8 +1,9 @@
 {
   "entries": [
-    {"dir": "contact", "name": "Contact", "navbar": true},
     {"dir": "jsdoc", "name": "JSDoc", "navbar": true},
     {"dir": "sphinx", "name": "Sphinx", "navbar": true},
+    {"dir": "contact", "name": "Contact", "navbar": true},
+    {"dir": "my_account", "name": "My account", "navbar": true},
     {"dir": "search", "name": "Search"}
   ]
 }
diff --git a/_svg/icon_password_reset.svg b/_svg/icon_password_reset.svg
new file mode 100644 (file)
index 0000000..9906e7a
--- /dev/null
@@ -0,0 +1,257 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="128"
+   height="128"
+   viewBox="0 0 64 64"
+   version="1.1"
+   id="svg31"
+   sodipodi:docname="icon_password_reset.svg"
+   inkscape:version="1.1 (1:1.1+202105261517+ce6663b3b7)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <defs
+     id="defs24">
+    <inkscape:path-effect
+       effect="bspline"
+       id="path-effect12759"
+       is_visible="true"
+       lpeversion="1"
+       weight="33.333333"
+       steps="2"
+       helper_size="0"
+       apply_no_weight="true"
+       apply_with_weight="true"
+       only_selected="false" />
+    <filter
+       style="color-interpolation-filters:sRGB"
+       inkscape:label="Drop Shadow"
+       id="filter961"
+       x="-0.13054262"
+       y="-0.17431795"
+       width="1.315478"
+       height="1.3957831">
+      <feFlood
+         flood-opacity="0.498039"
+         flood-color="rgb(0,0,0)"
+         result="flood"
+         id="feFlood951" />
+      <feComposite
+         in="flood"
+         in2="SourceGraphic"
+         operator="in"
+         result="composite1"
+         id="feComposite953" />
+      <feGaussianBlur
+         in="composite1"
+         stdDeviation="0.7071067811865476"
+         result="blur"
+         id="feGaussianBlur955" />
+      <feOffset
+         dx="0.7071067811865476"
+         dy="0.7071067811865476"
+         result="offset"
+         id="feOffset957" />
+      <feComposite
+         in="SourceGraphic"
+         in2="offset"
+         operator="over"
+         result="composite2"
+         id="feComposite959" />
+    </filter>
+    <filter
+       style="color-interpolation-filters:sRGB;"
+       inkscape:label="Drop Shadow"
+       id="filter868"
+       x="-0.64166667"
+       y="-0.42777778"
+       width="2.7833333"
+       height="2.1888889">
+      <feFlood
+         flood-opacity="0.498039"
+         flood-color="rgb(0,0,0)"
+         result="flood"
+         id="feFlood858" />
+      <feComposite
+         in="flood"
+         in2="SourceGraphic"
+         operator="in"
+         result="composite1"
+         id="feComposite860" />
+      <feGaussianBlur
+         in="composite1"
+         stdDeviation="3"
+         result="blur"
+         id="feGaussianBlur862" />
+      <feOffset
+         dx="6"
+         dy="6"
+         result="offset"
+         id="feOffset864" />
+      <feComposite
+         in="SourceGraphic"
+         in2="offset"
+         operator="over"
+         result="composite2"
+         id="feComposite866" />
+    </filter>
+    <filter
+       style="color-interpolation-filters:sRGB"
+       inkscape:label="Drop Shadow"
+       id="filter961-3"
+       x="-0.54926406"
+       y="-0.62773036"
+       width="2.2753048"
+       height="2.4574912">
+      <feFlood
+         flood-opacity="0.498039"
+         flood-color="rgb(0,0,0)"
+         result="flood"
+         id="feFlood951-6" />
+      <feComposite
+         in="flood"
+         in2="SourceGraphic"
+         operator="in"
+         result="composite1"
+         id="feComposite953-7" />
+      <feGaussianBlur
+         in="composite1"
+         stdDeviation="0.7071067811865476"
+         result="blur"
+         id="feGaussianBlur955-5" />
+      <feOffset
+         dx="0.7071067811865476"
+         dy="0.7071067811865476"
+         result="offset"
+         id="feOffset957-3" />
+      <feComposite
+         in="SourceGraphic"
+         in2="offset"
+         operator="over"
+         result="composite2"
+         id="feComposite959-5" />
+    </filter>
+  </defs>
+  <metadata
+     id="metadata35">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1016"
+     id="namedview33"
+     showgrid="true"
+     inkscape:zoom="1.3037281"
+     inkscape:cx="88.208575"
+     inkscape:cy="92.04373"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg31"
+     inkscape:measure-start="0,0"
+     inkscape:measure-end="0,0"
+     inkscape:snap-text-baseline="true"
+     inkscape:pagecheckerboard="0"
+     inkscape:snap-smooth-nodes="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid821"
+       empspacing="4" />
+  </sodipodi:namedview>
+  <g
+     id="text870"
+     style="fill:#ff0000;fill-opacity:1;stroke:none;filter:url(#filter961)"
+     transform="translate(2,2)" />
+  <g
+     id="text882"
+     style="fill:#0000ff;fill-opacity:1;stroke:none;filter:url(#filter961)"
+     transform="translate(2,2)" />
+  <rect
+     style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter961-3)"
+     id="rect11777"
+     width="42"
+     height="8"
+     x="10"
+     y="14" />
+  <rect
+     style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter961-3)"
+     id="rect11777-2"
+     width="42"
+     height="8"
+     x="10"
+     y="34" />
+  <circle
+     style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter961-3)"
+     id="path12014"
+     cx="18"
+     cy="38"
+     r="1" />
+  <circle
+     style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter961-3)"
+     id="path12014-9"
+     cx="14"
+     cy="38"
+     r="1" />
+  <circle
+     style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter961-3)"
+     id="path12014-1"
+     cx="22"
+     cy="38"
+     r="1" />
+  <circle
+     style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter961-3)"
+     id="path12014-2"
+     cx="26"
+     cy="38"
+     r="1" />
+  <circle
+     style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter961-3)"
+     id="path12014-7"
+     cx="30"
+     cy="38"
+     r="1" />
+  <path
+     style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter961-3)"
+     id="path12579"
+     sodipodi:type="arc"
+     sodipodi:cx="52"
+     sodipodi:cy="54"
+     sodipodi:rx="6"
+     sodipodi:ry="6"
+     sodipodi:start="0"
+     sodipodi:end="5.4977871"
+     sodipodi:arc-type="arc"
+     d="m 58,54 a 6,6 0 0 1 -4.829458,5.884712 6,6 0 0 1 -6.713819,-3.588611 6,6 0 0 1 2.209855,-7.284919 6,6 0 0 1 7.576063,0.746177"
+     sodipodi:open="true" />
+  <path
+     style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter961-3)"
+     d="m 57,47 c 0,0 0,3.5 0,3.5 0,0 -4,0 -4,0"
+     id="path12757"
+     inkscape:path-effect="#path-effect12759"
+     inkscape:original-d="m 57,47 v 3.5 h -4"
+     sodipodi:nodetypes="ccc" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:square;filter:url(#filter961-3)"
+     d="m 10,10 h 12 m 0,20 H 10"
+     id="path1744"
+     sodipodi:nodetypes="cccc" />
+</svg>
diff --git a/_svg/icon_sign_up.svg b/_svg/icon_sign_up.svg
new file mode 100644 (file)
index 0000000..9a3cf6d
--- /dev/null
@@ -0,0 +1,243 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="128"
+   height="128"
+   viewBox="0 0 64 64"
+   version="1.1"
+   id="svg31"
+   sodipodi:docname="icon_sign_up.svg"
+   inkscape:version="1.1 (1:1.1+202105261517+ce6663b3b7)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <defs
+     id="defs24">
+    <inkscape:path-effect
+       effect="bspline"
+       id="path-effect1268"
+       is_visible="true"
+       lpeversion="1"
+       weight="33.333333"
+       steps="2"
+       helper_size="0"
+       apply_no_weight="true"
+       apply_with_weight="true"
+       only_selected="false" />
+    <inkscape:path-effect
+       effect="bspline"
+       id="path-effect1119"
+       is_visible="true"
+       lpeversion="1"
+       weight="33.333333"
+       steps="2"
+       helper_size="0"
+       apply_no_weight="true"
+       apply_with_weight="true"
+       only_selected="false" />
+    <filter
+       style="color-interpolation-filters:sRGB"
+       inkscape:label="Drop Shadow"
+       id="filter961"
+       x="-0.18308802"
+       y="-inf"
+       width="1.4251016"
+       height="inf">
+      <feFlood
+         flood-opacity="0.498039"
+         flood-color="rgb(0,0,0)"
+         result="flood"
+         id="feFlood951" />
+      <feComposite
+         in="flood"
+         in2="SourceGraphic"
+         operator="in"
+         result="composite1"
+         id="feComposite953" />
+      <feGaussianBlur
+         in="composite1"
+         stdDeviation="0.7071067811865476"
+         result="blur"
+         id="feGaussianBlur955" />
+      <feOffset
+         dx="0.7071067811865476"
+         dy="0.7071067811865476"
+         result="offset"
+         id="feOffset957" />
+      <feComposite
+         in="SourceGraphic"
+         in2="offset"
+         operator="over"
+         result="composite2"
+         id="feComposite959" />
+    </filter>
+    <filter
+       style="color-interpolation-filters:sRGB;"
+       inkscape:label="Drop Shadow"
+       id="filter868"
+       x="-0.64166667"
+       y="-inf"
+       width="2.7833333"
+       height="inf">
+      <feFlood
+         flood-opacity="0.498039"
+         flood-color="rgb(0,0,0)"
+         result="flood"
+         id="feFlood858" />
+      <feComposite
+         in="flood"
+         in2="SourceGraphic"
+         operator="in"
+         result="composite1"
+         id="feComposite860" />
+      <feGaussianBlur
+         in="composite1"
+         stdDeviation="3"
+         result="blur"
+         id="feGaussianBlur862" />
+      <feOffset
+         dx="6"
+         dy="6"
+         result="offset"
+         id="feOffset864" />
+      <feComposite
+         in="SourceGraphic"
+         in2="offset"
+         operator="over"
+         result="composite2"
+         id="feComposite866" />
+    </filter>
+    <filter
+       style="color-interpolation-filters:sRGB"
+       inkscape:label="Drop Shadow"
+       id="filter961-3"
+       x="-0.36617604"
+       y="-0.36617604"
+       width="1.8502032"
+       height="1.8502032">
+      <feFlood
+         flood-opacity="0.498039"
+         flood-color="rgb(0,0,0)"
+         result="flood"
+         id="feFlood951-6" />
+      <feComposite
+         in="flood"
+         in2="SourceGraphic"
+         operator="in"
+         result="composite1"
+         id="feComposite953-7" />
+      <feGaussianBlur
+         in="composite1"
+         stdDeviation="0.7071067811865476"
+         result="blur"
+         id="feGaussianBlur955-5" />
+      <feOffset
+         dx="0.7071067811865476"
+         dy="0.7071067811865476"
+         result="offset"
+         id="feOffset957-3" />
+      <feComposite
+         in="SourceGraphic"
+         in2="offset"
+         operator="over"
+         result="composite2"
+         id="feComposite959-5" />
+    </filter>
+  </defs>
+  <metadata
+     id="metadata35">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1016"
+     id="namedview33"
+     showgrid="true"
+     inkscape:zoom="3.6875"
+     inkscape:cx="84.610169"
+     inkscape:cy="57.220339"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg31"
+     inkscape:measure-start="0,0"
+     inkscape:measure-end="0,0"
+     inkscape:snap-text-baseline="true"
+     inkscape:pagecheckerboard="0"
+     inkscape:snap-smooth-nodes="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid821"
+       empspacing="4" />
+  </sodipodi:namedview>
+  <g
+     id="text870"
+     style="fill:#ff0000;fill-opacity:1;stroke:none;filter:url(#filter961)" />
+  <g
+     id="text882"
+     style="fill:#0000ff;fill-opacity:1;stroke:none;filter:url(#filter961)" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter961)"
+     id="path224"
+     sodipodi:type="arc"
+     sodipodi:cx="32"
+     sodipodi:cy="52"
+     sodipodi:rx="26"
+     sodipodi:ry="14"
+     sodipodi:start="3.1415927"
+     sodipodi:end="4.3545965"
+     sodipodi:arc-type="arc"
+     d="M 6,52 A 26,14 0 0 1 22.894608,38.886589"
+     sodipodi:open="true" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter961-3)"
+     id="path224-6"
+     sodipodi:type="arc"
+     sodipodi:cx="32"
+     sodipodi:cy="52"
+     sodipodi:rx="26"
+     sodipodi:ry="14"
+     sodipodi:start="5.0701815"
+     sodipodi:end="0"
+     sodipodi:arc-type="arc"
+     sodipodi:open="true"
+     d="M 41.105392,38.886589 A 26,14 0 0 1 58,52" />
+  <circle
+     style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter961)"
+     id="path1487"
+     cx="32"
+     cy="28"
+     r="14" />
+  <path
+     style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:square;stroke-opacity:1;filter:url(#filter961-3)"
+     d="m 48,16 c 0,0 6,0 6,0 0,0 0,6 0,6"
+     id="path1117"
+     inkscape:path-effect="#path-effect1119"
+     inkscape:original-d="m 48,16 h 6 v 6"
+     sodipodi:nodetypes="ccc" />
+  <path
+     style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:square;stroke-opacity:1;filter:url(#filter961-3)"
+     d="m 54,10 c 0,0 0,6 0,6 0,0 6,0 6,0"
+     id="path1117-3"
+     inkscape:path-effect="#path-effect1268"
+     inkscape:original-d="m 54,10 v 6 h 6"
+     sodipodi:nodetypes="ccc" />
+</svg>
diff --git a/api/errors.json b/api/errors.json
new file mode 100644 (file)
index 0000000..28b7e37
--- /dev/null
@@ -0,0 +1,22 @@
+{
+  "400": "Bad request",
+  "401": "Unauthorized",
+  "402": "Payment required",
+  "403": "Forbidden",
+  "404": "Not found",
+  "405": "Method not allowed",
+  "406": "Not acceptable",
+  "407": "Proxy authentication required",
+  "408": "Request timeout",
+  "409": "Conflict",
+  "410": "Gone",
+  "411": "Length required",
+  "412": "Precondition failed",
+  "413": "Request entity too large",
+  "414": "Request URI too long",
+  "415": "Unsupported media type",
+  "417": "Expectation failed",
+  "418": "No verification image in session",
+  "419": "Verification code mismatch",
+  "420": "Account already exists"
+}
diff --git a/api/sign_up.js.min b/api/sign_up.js.min
new file mode 100644 (file)
index 0000000..e4140d4
--- /dev/null
@@ -0,0 +1,28 @@
+/*let*/ sign_up = async (
+  email,
+  verification_code,
+  given_names,
+  family_name,
+  password,
+  contact_me
+) => {
+  let response = await fetch(
+    '/api/sign_up.json',
+    {
+      method: 'POST',
+      body: JSON.stringify(
+        {
+          email,
+          verification_code,
+          given_names,
+          family_name,
+          password,
+          contact_me
+        }
+      )
+    }
+  )
+  if (!response.ok)
+    throw new Error((await response.json()).detail)
+  return /*await*/ response.json()
+}
diff --git a/api/sign_up.json.jst b/api/sign_up.json.jst
new file mode 100644 (file)
index 0000000..2344074
--- /dev/null
@@ -0,0 +1,150 @@
+let stream_buffers = require('stream-buffers')
+let XDate = require('xdate')
+
+return async env => {
+  let session_cookie = await _require('/session_cookie.jst')
+
+  if (env.request.method !== 'POST') {
+    env.response.setHeader('Allow', 'POST')
+    env.mime_type = 'application/problem+json; charset=utf-8'
+    env.site.serve(
+      env,
+      405,
+      Buffer.from(
+        JSON.stringify(
+          {
+            title: 'Method not allowed',
+            detail: `The endpoint "${env.parsed_url.path}" requires a POST request.`,
+            status: 405
+          },
+          null,
+          2
+        ) + '\n',
+        'utf-8'
+      )
+    )
+    return
+  }
+
+  let write_stream = new stream_buffers.WritableStreamBuffer()
+  let data = new Promise(
+    (resolve, reject) => {
+      write_stream.
+      on('finish', () => {resolve(write_stream.getContents())}).
+      on('error', () => {reject()})
+    }
+  )
+  env.request.pipe(write_stream)
+  let query = JSON.parse((await data).toString())
+  let email = query.email.toLowerCase()
+  console.log('sign up', email)
+
+  // initialize env.session_key, set cookie in env.response
+  let transaction = env.site.database.Transaction()
+  let session = await session_cookie(env, transaction)
+
+  let captcha = await session.get('captcha')
+  if (captcha === undefined || XDate.now() >= captcha.get('expires')) {
+    transaction.rollback()
+
+    env.mime_type = 'application/problem+json; charset=utf-8'
+    env.site.serve(
+      env,
+      418,
+      Buffer.from(
+        JSON.stringify(
+          {
+            title: 'No verification image in session',
+            detail: `Please call the "/api/verification_image.png" endpoint to create a verification image, in same session as the "/api/sign_up.json" call and less than one hour prior.`,
+            status: 418
+          },
+          null,
+          2
+        ) + '\n',
+        'utf-8'
+      )
+    )
+    return
+  }
+
+  
+  let verification_code = query.verification_code.toLowerCase()
+  let captcha_text = await captcha.get('text')
+  if (verification_code !== captcha_text) {
+    console.log(`verification code mismatch, \"${verification_code}\" should be \"${captcha_text}\"`)
+    transaction.rollback()
+
+    env.mime_type = 'application/problem+json; charset=utf-8'
+    env.site.serve(
+      env,
+      419,
+      Buffer.from(
+        JSON.stringify(
+          {
+            title: 'Verification code mismatch',
+            detail: `The provided verification code "${verification_code}" did not match the verification image.`,
+            status: 419
+          },
+          null,
+          2
+        ) + '\n',
+        'utf-8'
+      )
+    )
+    return
+  }
+
+  let accounts = await (
+    await transaction.get({})
+  ).get('accounts', {})
+
+  if (accounts.has(email)) {
+    transaction.rollback()
+
+    env.mime_type = 'application/problem+json; charset=utf-8'
+    env.site.serve(
+      env,
+      420,
+      Buffer.from(
+        JSON.stringify(
+          {
+            title: 'Account already exists',
+            detail: `The email "${email}" already has an account registered.`,
+            status: 420
+          },
+          null,
+          2
+        ) + '\n',
+        'utf-8'
+      )
+    )
+    return
+  }
+  accounts.set(
+    email,
+    transaction.json_to_logjson(
+      {
+        given_names: query.given_names || '',
+        family_name: query.family_name || '',
+        password: query.password || '',
+        contact_me: query.contact_me || false,
+        email_verified: false
+      }
+    )
+  )
+
+  await transaction.commit()
+
+  env.site.serve(
+    env,
+    200,
+    Buffer.from(
+      JSON.stringify(
+        null,
+        null,
+        2
+      ) + '\n',
+      'utf-8'
+    )
+  )
+}
diff --git a/api/verification_image.png.jst b/api/verification_image.png.jst
new file mode 100644 (file)
index 0000000..413b405
--- /dev/null
@@ -0,0 +1,34 @@
+let captchagen = require('captchagen')
+let util = require('util')
+let XDate = require('xdate')
+
+return async env => {
+  let session_cookie = await _require('/session_cookie.jst')
+
+  let captcha = captchagen.create()
+  captcha.generate()
+
+  // initialize env.session_key, set cookie in env.response
+  let transaction = env.site.database.Transaction()
+  let session = await session_cookie(env, transaction)
+
+  // store captcha text in the session for validation when form submitted
+  let expires = new XDate()
+  expires.addHours(1)
+  session.set(
+    'captcha',
+    transaction.json_to_logjson(
+      {
+        text: captcha.text(),
+        expires: expires.getTime()
+      }
+    )
+  )
+
+  await transaction.commit()
+
+  // serve the png file
+  env.caching = false
+  let captcha_buffer = util.promisify(captcha.buffer).bind(captcha)
+  env.site.serve(env, 200, await captcha_buffer(), 'verification_image.png.jst')
+}
index febc7b0..f3c48fa 100644 (file)
 }
 
 // see https://stackoverflow.com/questions/20547819/vertical-align-with-bootstrap-3
+.vcenter {
+  display: inline-block;
+  vertical-align: center;
+  float: none;
+}
 .vbottom {
   display: inline-block;
   vertical-align: bottom;
index 767ce36..5cf253c 100644 (file)
@@ -53,7 +53,7 @@
   background-color: @modal-content-bg;
   border: 1px solid @modal-content-fallback-border-color; //old browsers fallback (ie8 etc)
   border: 1px solid @modal-content-border-color;
-  border-radius: @border-radius-large;
+  border-radius: @modal-border-radius; //border-radius-large;
   .box-shadow(0 3px 9px rgba(0,0,0,.5));
   background-clip: padding-box;
   // Remove focus outline from opened modal
index 5506501..4ddf111 100644 (file)
 //
 //##
 
+@modal-border-radius:         @border-radius-base; // Nick @border-radius-large;
+
 //** Padding applied to the modal body
-@modal-inner-padding:         15px;
+@modal-inner-padding:         10px; // Nick 15px;
 
 //** Padding applied to the modal title
-@modal-title-padding:         15px;
+@modal-title-padding:         10px; // Nick 15px;
 //** Modal title line-height
 @modal-title-line-height:     @line-height-base;
 
diff --git a/my_account/_menu.json b/my_account/_menu.json
new file mode 100644 (file)
index 0000000..5cf9006
--- /dev/null
@@ -0,0 +1,16 @@
+{
+  "entries": [
+    {
+      "dir": "sign_up",
+      "name": "Sign up",
+      "icon": "/_svg/icon_sign_up.svg"
+    },
+    {"dir": "verify_email", "name": "Verify email"},
+    {
+      "dir": "password_reset",
+      "name": "Password reset",
+      "icon": "/_svg/icon_password_reset.svg"
+    },
+    {"dir": "verify_password", "name": "Verify password"}
+  ]
+}
diff --git a/my_account/index.html.jst b/my_account/index.html.jst
new file mode 100644 (file)
index 0000000..234367b
--- /dev/null
@@ -0,0 +1,501 @@
+let querystring = require('querystring')
+let stream_buffers = require('stream-buffers')
+
+return async env => {
+  //let account = await _require('/account.jst')
+  let breadcrumbs = await _require('/breadcrumbs.jst')
+  let menu = await env.site.get_menu('/my_account/_menu.json')
+  let navbar = await _require('/navbar.jst')
+
+  await navbar(
+    env,
+    // head
+    async _out => {},
+    // body
+    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 {
+      //            input.btn.btn-success.btn-send(type="submit" value="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 {
+      //            input.btn.btn-success.btn-send(type="submit" value="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 {
+      //            input.btn.btn-success.btn-send(type="submit" value="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 {
+      //            input.btn.btn-success.btn-send(type="submit" value="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 {
+      //            input.btn.btn-success.btn-send(type="submit" value="Change") {}
+      //          }
+      //        }
+      //      }
+      //    }
+      //  }
+//
+      //  div.row {
+      //    div.col-md-12 {
+      //      p.text-muted {
+      //        strong {'*'}
+      //        'These fields are required.'
+      //      }
+      //    }
+      //  }
+      //}
+      //else {
+        // signed out
+        p {'For account maintenance, please click on one of the options below.'}
+
+        ul.nav.nav-stacked {
+          let entries = menu.entries
+          for (let i = 0; i < entries.length; ++i) {
+            let entry = entries[i]
+            if (Object.prototype.hasOwnProperty.call(entry, 'icon'))
+              li {
+                a(href=`${entry.dir}/index.html`) {
+                  table.icon-and-text {
+                    tr {
+                      td {
+                        _out.push(await env.site.get_min_svg(entry.icon))
+                      }
+                      td {
+                        span.text-h2{`${entry.name}`}
+                      }
+                    }
+                  }
+                }
+              }
+          }
+        }
+      //}
+    },
+    // scripts
+    async _out => {
+      script {
+        // this will be called by navbar logic after sign in/out
+        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')
+                    }
+                  }
+                )
+              }
+            )
+
+            // 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')
+                    }
+                  }
+                )
+              }
+            )
+
+            // 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')
+                    }
+                  }
+                )
+              }
+            )
+
+            // 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')
+                    }
+                  }
+                )
+              }
+            )
+
+            //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')
+                    }
+                  }
+                )
+              }
+            )
+          }
+        )
+      }
+    }
+  )
+}
diff --git a/my_account/sign_up/index.html.jst b/my_account/sign_up/index.html.jst
new file mode 100644 (file)
index 0000000..5d88ae4
--- /dev/null
@@ -0,0 +1,150 @@
+return async env => {
+  let breadcrumbs = await _require('/breadcrumbs.jst')
+  let navbar = await _require('/navbar.jst')
+
+  await navbar(
+    env,
+    // head
+    async _out => {},
+    // body
+    async _out => {
+      await breadcrumbs(env, _out)
+
+      p {'Signing up allows you to leave comments on our blog and receive communications from us.'}
+
+      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-6 {
+          div.form-group {
+            label(for="given-names") {'Given names *'}
+            input.form-control#given-names(type="text" placeholder="Your given names" required="required" maxlength=256) {}
+          }
+        }
+        div.col-md-6 {
+          div.form-group {
+            label(for="family-name") {'Family name'}
+            input.form-control#family-name(type="text" placeholder="Your family name" maxlength=256) {}
+          }
+        }
+      }
+      div.row {
+        div.col-md-6 {
+          div.form-group {
+            label(for="email") {'Email *'}
+            input.form-control#email(type="email" placeholder="Your email address" required="required" maxlength=256) {}
+          }
+        }
+        div.col-md-6 {
+          div.form-group {
+            label(for="password") {'Password *'}
+            input.form-control#password(type="password" placeholder="New password" required="required" minlength=8 maxlength=256) {}
+          }
+        }
+      }
+      div.row {
+        div.col-md-12 {
+          div.form-group {
+            input.form-check-input#contact-me(type="checkbox" checked="checked") {}
+            ' '
+            label(for="contact-me") {
+              'Contact me by email with updates and special offers'
+            }
+          }
+        }
+      }
+      div.row {
+        div.'col-md-6'.vcenter {
+          div.form-group {
+            label(for="verification") {'Verification *'}
+            input.form-control#verification(type="text" placeholder="Type the verification code shown to the right" required="required" minlength=6 maxlength=6) {}
+          }
+        }
+        div.'col-md-3'.vcenter {
+          img#verification-image(src="/api/verification_image.png" width=300 height=150) {}
+        }
+        div.'col-md-3'.vcenter.text-center {
+          input.btn.btn-default#'generate-new-code'(type="button" value="Generate new code") {}
+        }
+      }
+      div.row {
+        div.col-md-12 {
+          input.btn.btn-success#submit(type="button" value="Create account") {}
+        }
+      }
+      p {} // fix this later
+      p.text-muted {
+        strong {'*'}
+        'These fields are required.'
+      }
+
+      // hidden part
+      div#message-modal.modal.fade(role="dialog") {
+        div.modal-dialog {
+          div.modal-content {
+            div.modal-header {
+              //button.close(type="button" data-dismiss="modal") {
+              //  '×'
+              //}
+              h5.modal-title {'Message'}
+            }
+            div#message-modal-message.modal-body {
+            }
+            div.modal-footer {
+              button.btn.btn-default(type="button" data-dismiss="modal") {
+                'Close'
+              }
+            }
+          }
+        }
+      }
+    },
+    // scripts
+    async _out => {
+      script(src="/api/sign_up.js") {}
+
+      script {
+        $(document).ready(
+          () => {
+            $('#submit').click(
+              async () => {
+                if (
+                  document.getElementById('given-names').reportValidity() &&
+                    document.getElementById('family-name').reportValidity() &&
+                    document.getElementById('email').reportValidity() &&
+                    document.getElementById('password').reportValidity() &&
+                    document.getElementById('verification').reportValidity()
+                ) {
+                  try {
+                    await sign_up(
+                      document.getElementById('email').value,
+                      document.getElementById('verification').value
+                      document.getElementById('given-names').value,
+                      document.getElementById('family-name').value,
+                      document.getElementById('password').value
+                    )
+                  }
+                  catch (e) {
+                    $('#message-modal-message').text(e.message)
+                    $('#message-modal').modal('show')
+                    return
+                  }
+                  $('#message-modal-message').text(`Your account with email "${document.getElementById('email').value}" has been created.`)
+                  $('#message-modal').modal('show')
+                } 
+              }
+            )
+
+            let image_seq = 0
+            $('#generate-new-code').click(
+              () => {
+                document.getElementById('verification-image').src = `/api/verification_image.png?seq=${image_seq}`
+                image_seq += 1
+              }
+            ) 
+          }
+        )
+      }
+    }
+  )
+}
index 52a4ddc..cec56d9 100644 (file)
@@ -7,12 +7,12 @@
     "@ndcode/emailjs_cache": "^0.1.0",
     "@ndcode/logjson": "^0.1.0",
     "@ndcode/zettair_cache": "^0.1.0",
+    "captchagen": "^1.2.0",
     "cookie": "^0.3.1",
     "querystring": "^0.2.0",
     "stream-buffers": "^3.0.2",
     "xdate": "^0.8.2"
   },
-  "devDependencies": {},
   "scripts": {},
   "author": "Nick Downing",
   "license": "CC-BY-SA-3.0"
index b631d3f..2d1e3dc 100644 (file)
--- a/page.jst
+++ b/page.jst
@@ -5,32 +5,17 @@ let crypto = require('crypto')
 return async (env, head, body, scripts) => {
   let favicons = await env.site.get_min_html('/_favicon/favicons.html')
   let globals = await env.site.get_json('/_config/globals.json')
+  let session_cookie = await _require('/session_cookie.jst')
 
+  // initialize env.session_key, set cookie in env.response
   let transaction = env.site.database.Transaction()
-  let root = await transaction.get({})
-  let sessions = await root.get('sessions', {})
-  let pageviews = await root.get('pageviews', {})
+  let session = await session_cookie(env, transaction)
 
-  let cookies = cookie.parse(env.request.headers.cookie || '')
-  let session_key =
-    Object.prototype.hasOwnProperty.call(cookies, 'session_key') ?
-      cookies.session_key :
-      crypto.randomBytes(16).toString('hex')
-  let session = await sessions.get(session_key, {})
-    
-  let expires = new XDate()
-  expires.addMonths(1)
-  session.set('expires', expires.toUTCString())
-  env.response.setHeader(
-    'Set-Cookie',
-    'session_key=' +
-    session_key +
-    '; expires=' +
-    session.expires +
-    '; path=/;'
-  )
-
-  let pageview = await pageviews.get(env.parsed_url.pathname, {})
+  let pageview = await (
+    await (
+      await transaction.get({})
+    ).get('pageviews', {})
+  ).get(env.parsed_url.pathname, {})
   pageview.set('visits', (await pageview.get('visits') || 0) + 1)
 
   let session_pageviews = await session.get('pageviews', {})
diff --git a/session_cookie.jst b/session_cookie.jst
new file mode 100644 (file)
index 0000000..b0f09bf
--- /dev/null
@@ -0,0 +1,43 @@
+let XDate = require('xdate')
+let cookie = require('cookie')
+let crypto = require('crypto')
+
+return async (env, transaction) => {
+  let cookies = cookie.parse(env.request.headers.cookie || '')
+  let now = Date.now()
+
+  let sessions = await (
+    await transaction.get({})
+  ).get('sessions', {})
+
+  let session_key, session, expires = new XDate(now)
+  if (
+    Object.prototype.hasOwnProperty.call(cookies, 'session_key') &&
+      (
+        session = await sessions.get(session_key = cookies.session_key)
+      ) !== undefined &&
+      now < await session.get('expires', 0)
+  )
+    // if session key is already in database, we know the requester supports
+    // cookies, therefore each access extends the session expiry by 1 month
+    expires.addMonths(1)
+  else {
+    // first request for session, maybe a bot, retain session for only 1 day
+    expires.addDays(1)
+    do {
+      session_key = crypto.randomBytes(16).toString('hex')
+    } while (sessions.has(session_key))
+    session = transaction.LazyObject()
+    sessions.set(session_key, session)
+  }
+
+  await session.set('expires', expires.getTime())
+
+  env.response.setHeader(
+    'Set-Cookie',
+    `session_key=${session_key}; expires=${expires.toUTCString()}; path=/;`
+  )
+  env.session_key = session_key
+
+  return session
+}