Add expiry on sign up draft (to prevent leakage of personal information)
[ndcode_site.git] / my_account / sign_up / index.html.jst
1 let logjson = (await import('@ndcode/logjson')).default
2 let XDate = require('xdate')
3
4 return async env => {
5   let breadcrumbs = await _require('/_lib/breadcrumbs.jst')
6   let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
7   let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
8   let navbar = await _require('/_lib/navbar.jst')
9   let session_cookie = await _require('/_lib/session_cookie.jst')
10
11   // preload draft details if any
12   let transaction = await env.site.database.Transaction(), draft_details
13   try {
14     // initialize env.session_key, set cookie in env.response
15     let session = await session_cookie(env, transaction)
16
17     let sign_up_draft = await session.get('sign_up_draft')
18     draft_details =
19       sign_up_draft !== undefined &&
20         XDate.now() < await logjson.logjson_to_json(
21           await sign_up_draft.get('expires')
22         ) ? {
23           email: await logjson.logjson_to_json(
24             await sign_up_draft.get('email')
25           ),
26           given_names: await logjson.logjson_to_json(
27             await sign_up_draft.get('given_names')
28           ),
29           family_name: await logjson.logjson_to_json(
30             await sign_up_draft.get('family_name')
31           ),
32           contact_me: await logjson.logjson_to_json(
33             await sign_up_draft.get('contact_me')
34           )
35         } : null
36
37     await transaction.commit()
38   }
39   catch (error) {
40     transaction.rollback()
41     throw error
42   }
43   console.log('draft_details', JSON.stringify(draft_details))
44
45   await navbar(
46     env,
47     // head
48     async _out => {},
49     // body
50     async _out => {
51       await breadcrumbs(env, _out)
52
53       p {'Signing up allows you to leave comments on our blog and receive communications from us.'}
54
55       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.'}
56
57       div.accordion#accordion.mb-5(role="tablist" aria-multiselectable="true") {
58         div.card#step-1 {
59           div.card-header#step-1-heading(role="tab") {
60             span#step-1-tick(style="display: none;") {
61               span.icon-color.pr-3 {_out.push(icon_tick)}
62             }
63             span#step-1-cross(style="display: none;") {
64               span.icon-color.pr-3 {_out.push(icon_cross)}
65             }
66             //span#step-1-spinner(style="display: none;") {
67             //  span.icon-color.pr-3 {
68             //    div.spinner-border(role="status") {
69             //      span.sr-only {'Loading...'}
70             //    }
71             //  }
72             //}
73             a.h5(data-toggle="collapse" data-parent="#accordion" href="#step-1-collapse" aria-expanded="true" aria-controls="step-1-collapse") {
74               'Your details'
75             }
76           }
77           div#step-1-collapse.collapse.show(role="tabpanel" aria-labelledby="step-1-heading" data-parent="#accordion") {
78             div.card-body {
79               div.row {
80                 div.col-md-6 {
81                   div.form-group {
82                     label.form-label(for="given-names") {'Given names *'}
83                     input.form-control#given-names(type="text" value=draft_details ? draft_details.given_names : '' placeholder="Your given names" required="required" maxlength=256) {}
84                   }
85                 }
86                 div.col-md-6 {
87                   div.form-group {
88                     label.form-label(for="family-name") {'Family name'}
89                     input.form-control#family-name(type="text" value=draft_details ? draft_details.family_name : '' placeholder="Your family name" maxlength=256) {}
90                   }
91                 }
92               }
93               div.row {
94                 div.col-md-6 {
95                   div.form-group {
96                     label.form-label(for="email") {'Email *'}
97                     input.form-control#email(type="email" value=draft_details ? draft_details.email : '' placeholder="Your email address" required="required" maxlength=256) {}
98                   }
99                 }
100                 div.col-md-6 {
101                   div.form-group {
102                     label.form-label(for="password") {'Password *'}
103                     input.form-control#password(type="password" placeholder="New password" required="required" minlength=8 maxlength=256) {}
104                   }
105                 }
106               }
107               div.row {
108                 div.col-md-12 {
109                   div.custom-control.custom-checkbox {
110                     if (!draft_details || draft_details.contact_me)
111                       input.custom-control-input#contact-me(type="checkbox" checked="checked") {}
112                     else
113                       input.custom-control-input#contact-me(type="checkbox") {}
114                     ' '
115                     label.custom-control-label(for="contact-me") {
116                       'Contact me by email with updates and special offers'
117                     }
118                   }
119                 }
120               }
121               div.row.align-items-center.mb-3 {
122                 div.'col-md-6' {
123                   div.form-group {
124                     label.form-label(for="verification-code") {'Verification code *'}
125                     input.form-control#verification-code(type="text" placeholder="Type the code shown to the right" required="required" minlength=6 maxlength=6) {}
126                   }
127                 }
128                 div.'col-md-4' {
129                   img#verification-image(src="/api/verification_image.png?seq=0" width=300 height=150) {}
130                 }
131                 div.'col-md-2'.my-auto.text-center {
132                   button.btn.btn-outline-secondary#'step-1-new-code'(type="button") {'New code'}
133                 }
134               }
135
136               button.btn.btn-success#step-1-continue(type="button") {'Continue'}
137
138               p.'mt-3'.mb-0 {'* These fields are required.'}
139             }
140           }
141         }
142         div.card#step-2 {
143           div.card-header#step-2-heading(role="tab") {
144             span#step-2-tick(style="display: none;") {
145               span.icon-color.pr-3 {_out.push(icon_tick)}
146             }
147             span#step-2-cross(style="display: none;") {
148               span.icon-color.pr-3 {_out.push(icon_cross)}
149             }
150             span#step-2-spinner(style="display: none;") {
151               span.icon-color.pr-3 {
152                 div.spinner-border(role="status") {
153                   span.sr-only {'Loading...'}
154                 }
155               }
156             }
157             a.h5.collapsed(data-toggle="collapse" data-parent="#accordion" href="#step-2-collapse" aria-expanded="false" aria-controls="step-2-collapse") {
158               'Create account'
159             }
160           }
161           div#step-2-collapse.collapse(role="tabpanel" aria-labelledby="step-2-heading" data-parent="#accordion") {
162             div.card-body {
163               p#step-2-message {'Please enter your details first.'}
164
165               button.btn.btn-outline-secondary#step-2-back(type="button") {'Back'}
166               button.btn.btn-outline-secondary.ml-3#step-2-continue(type="button") {'Continue'}
167             }
168           }
169         }
170         div.card#step-3 {
171           div.card-header#step-3-heading(role="tab") {
172             span#step-3-tick(style="display: none;") {
173               span.icon-color.pr-3 {_out.push(icon_tick)}
174             }
175             span#step-3-cross(style="display: none;") {
176               span.icon-color.pr-3 {_out.push(icon_cross)}
177             }
178             span#step-3-spinner(style="display: none;") {
179               span.icon-color.pr-3 {
180                 div.spinner-border(role="status") {
181                   span.sr-only {'Loading...'}
182                 }
183               }
184             }
185             a.h5.collapsed(data-toggle="collapse" data-parent="#accordion" href="#step-3-collapse" aria-expanded="false" aria-controls="step-3-collapse") {
186               'Send email verification link'
187             }
188           }
189           div#step-3-collapse.collapse(role="tabpanel" aria-labelledby="step-3-heading" data-parent="#accordion") {
190             div.card-body {
191               p#step-3-message {'Please create your account first.'}
192
193               button.btn.btn-outline-secondary#step-3-back(type="button") {'Back'}
194               button.btn.btn-outline-secondary.ml-3#step-3-resend-email(type="button") {'Re-send email'}
195             }
196           }
197         }
198       }
199     },
200     // scripts
201     async _out => {
202       script(src="/js/api_call.js") {}
203
204       script {
205         let sign_up_create_account = async (...arguments) => api_call(
206           '/api/account/sign_up/create_account.json',
207           ...arguments
208         )
209         //let sign_up_get_draft = async (...arguments) => api_call(
210         //  '/api/account/sign_up/get_draft.json',
211         //  ...arguments
212         //)
213         let sign_up_set_draft = async (...arguments) => api_call(
214           '/api/account/sign_up/set_draft.json',
215           ...arguments
216         )
217         let sign_up_send_email_verification_link = async (...arguments) => api_call(
218           '/api/account/sign_up/send_email_verification_link.json',
219           ...arguments
220         )
221
222         let draft_timeout_running = false
223         let draft_timeout_handler = async () => {
224           draft_timeout_running = false
225           await sign_up_set_draft(
226             {
227               email: document.getElementById('email').value.slice(0, 256).toLowerCase(),
228               given_names: document.getElementById('given-names').value.slice(0, 256),
229               family_name: document.getElementById('family-name').value.slice(0, 256),
230               contact_me: document.getElementById('contact-me').checked ? true : false
231             }
232           )
233           //console.log('draft', await sign_up_get_draft())
234         }
235         let draft_change_handler = () => {
236           if (!draft_timeout_running) {
237             draft_timeout_running = true
238             setTimeout(draft_timeout_handler, 5000)
239           }
240         }
241
242         let details
243         let step_1 = async () => {
244           if (
245             !document.getElementById('given-names').reportValidity() ||
246               !document.getElementById('family-name').reportValidity() ||
247               !document.getElementById('email').reportValidity() ||
248               !document.getElementById('password').reportValidity() ||
249               !document.getElementById('verification-code').reportValidity()
250           ) {
251             $('#step-1-tick').hide()
252             $('#step-1-cross').show()
253             //$('#step-1-spinner').hide()
254             return false
255           }
256           $('#step-1-tick').show()
257           $('#step-1-cross').hide()
258           //$('#step-1-spinner').hide()
259
260           details = {
261             email: document.getElementById('email').value.slice(0, 256).toLowerCase(),
262             given_names: document.getElementById('given-names').value.slice(0, 256),
263             family_name: document.getElementById('family-name').value.slice(0, 256),
264             password: document.getElementById('password').value.slice(0, 256),
265             contact_me: document.getElementById('contact-me').checked ? true : false
266           }
267           return true
268         }
269
270         let step_2 = async () => {
271           $('#step-2-tick').hide()
272           $('#step-2-cross').hide()
273           $('#step-2-spinner').show()
274           document.getElementById('step-2').scrollIntoView()
275
276           try {
277             await sign_up_create_account(
278               // verification_code
279               document.getElementById('verification-code').value.slice(0, 6).toLowerCase(),
280               // details
281               details
282             )
283           }
284           catch (error) {
285             let problem =
286               error instanceof Problem ?
287                 error :
288                 new Problem(
289                   // title
290                   'Bad request',
291                   // detail
292                   (error.stack || error.message).toString()
293                   // status
294                   400
295                 )
296
297             $('#step-2-tick').hide()
298             $('#step-2-cross').show()
299             $('#step-2-spinner').hide()
300
301             document.getElementById('step-2-message').textContent = problem.detail
302             $('#step-2-collapse').collapse('show')
303             return false
304           }
305           $('#step-2-tick').show()
306           $('#step-2-cross').hide()
307           $('#step-2-spinner').hide()
308           document.getElementById('step-2-message').textContent = `Your account with email "${details.email}" has been created.`
309           return true
310         }
311
312         let step_3 = async () => {
313           $('#step-3-tick').hide()
314           $('#step-3-cross').hide()
315           $('#step-3-spinner').show()
316           document.getElementById('step-3').scrollIntoView()
317
318           try {
319             await sign_up_send_email_verification_link(details.email)
320           }
321           catch (error) {
322             let problem =
323               error instanceof Problem ?
324                 error :
325                 new Problem(
326                   // title
327                   'Bad request',
328                   // detail
329                   (error.stack || error.message).toString()
330                   // status
331                   400
332                 )
333
334             $('#step-3-tick').hide()
335             $('#step-3-cross').show()
336             $('#step-3-spinner').hide()
337
338             document.getElementById('step-3-message').textContent = problem.detail
339             $('#step-3-collapse').collapse('show')
340             return false
341           }
342           $('#step-3-tick').show()
343           $('#step-3-cross').hide()
344           $('#step-3-spinner').hide()
345
346           document.getElementById('step-3-message').textContent = `Email verification link has been sent to "${details.email}". Please check your email for next steps.`
347           return true
348         }
349
350         document.addEventListener(
351           'DOMContentLoaded',
352           () => {
353             document.getElementById('given-names').addEventListener(
354               'change',
355               draft_change_handler
356             )
357             document.getElementById('family-name').addEventListener(
358               'change',
359               draft_change_handler
360             )
361             document.getElementById('email').addEventListener(
362               'change',
363               draft_change_handler
364             )
365             document.getElementById('password').addEventListener(
366               'change',
367               draft_change_handler
368             )
369             document.getElementById('contact-me').addEventListener(
370               'change',
371               draft_change_handler
372             )
373
374             let image_seq = 1
375             document.getElementById('step-1-new-code').addEventListener(
376               'click',
377               () => {
378                 document.getElementById('verification-image').src = `/api/verification_image.png?seq=${image_seq}`
379                 image_seq += 1
380               }
381             )
382
383             document.getElementById('step-1-continue').addEventListener(
384               'click',
385               async () => {
386                 if (await step_1() && await step_2() && await step_3())
387                   $('#step-3-collapse').collapse('show')
388               }
389             )
390
391             document.getElementById('step-2-back').addEventListener(
392               'click',
393               () => {$('#step-1-collapse').collapse('show')}
394             )
395
396             document.getElementById('step-2-continue').addEventListener(
397               'click',
398               async () => {
399                 if (await step_3())
400                   $('#step-3-collapse').collapse('show')
401               }
402             )
403
404             document.getElementById('step-3-back').addEventListener(
405               'click',
406               () => {$('#step-2-collapse').collapse('show')}
407             )
408
409             document.getElementById('step-3-resend-email').addEventListener(
410               'click',
411               async () => {
412                 if (await step_3())
413                   $('#step-3-collapse').collapse('show')
414               }
415             )
416           }
417         )
418       }
419     }
420   )
421 }