From: Nick Downing Date: Sat, 15 Jan 2022 23:29:44 +0000 (+1100) Subject: Add a page to re-send email verification if signing in without verified email X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?p=ndcode_site.git;a=commitdiff_plain;h=c3ae811e2743c03b6f27f6ca0ee021558ad1f005 Add a page to re-send email verification if signing in without verified email --- diff --git a/_lib/navbar.jst b/_lib/navbar.jst index c6ff55f..ea1f056 100644 --- a/_lib/navbar.jst +++ b/_lib/navbar.jst @@ -360,6 +360,14 @@ return async (env, head, body, scripts) => { ) } catch (error) { + if ( + error instanceof Problem && + error.title === 'Email not yet verified' + ) { + location.href = `/my_account/send_verification_email?email=${encodeURIComponent(email)}` + return + } + document.getElementById('message-modal-message').textContent = error.message $('#sign-in-modal').modal('hide') $('#message-modal').modal('show') diff --git a/js/api_call.js.min b/js/api_call.js.min index 7ddced8..aa1db76 100644 --- a/js/api_call.js.min +++ b/js/api_call.js.min @@ -1,12 +1,18 @@ +Problem = class { + constructor(title, detail, status) { + this.title = title + this.detail = detail + this.status = status + } +} + api_call = async (endpoint, ...arguments) => { let response = await fetch( endpoint, - { - method: 'POST', - body: JSON.stringify(arguments) - } + {method: 'POST', body: JSON.stringify(arguments)} ) + let result = await response.json() if (!response.ok) - throw new Error((await response.json()).detail) - return /*await*/ response.json() + throw new Problem(result.title, result.detail, result.status) + return result } diff --git a/my_account/_menu.json b/my_account/_menu.json index 5cf9006..30cfa1a 100644 --- a/my_account/_menu.json +++ b/my_account/_menu.json @@ -5,6 +5,7 @@ "name": "Sign up", "icon": "/_svg/icon_sign_up.svg" }, + {"dir": "send_verification_email", "name": "Send verification email"}, {"dir": "verify_email", "name": "Verify email"}, { "dir": "password_reset", diff --git a/my_account/send_verification_email/index.html.jst b/my_account/send_verification_email/index.html.jst new file mode 100644 index 0000000..5d7c960 --- /dev/null +++ b/my_account/send_verification_email/index.html.jst @@ -0,0 +1,183 @@ +let logjson = (await import('@ndcode/logjson')).default + +return async env => { + let breadcrumbs = await _require('/_lib/breadcrumbs.jst') + let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg') + let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg') + let navbar = await _require('/_lib/navbar.jst') + let session_cookie = await _require('/_lib/session_cookie.jst') + + // preload draft details if any + let details = {} + if (Object.prototype.hasOwnProperty.call(env.parsed_url.query, 'email')) + details.email = decodeURIComponent(env.parsed_url.query.email) + console.log('details', JSON.stringify(details)) + + await navbar( + env, + // head + async _out => {}, + // body + async _out => { + await breadcrumbs(env, _out) + + p {'Please verify your email address before signing in. Check your email for next steps, or re-send the verification email below.'} + + div.accordion#accordion.mb-5(role="tablist" aria-multiselectable="true") { + div.card#step-1 { + div.card-header#step-1-heading(role="tab") { + span#step-1-tick(style="display: none;") { + span.icon-color.pr-3 {_out.push(icon_tick)} + } + span#step-1-cross(style="display: none;") { + span.icon-color.pr-3 {_out.push(icon_cross)} + } + //span#step-1-spinner(style="display: none;") { + // span.icon-color.pr-3 { + // div.spinner-border(role="status") { + // span.sr-only {'Loading...'} + // } + // } + //} + a.h5(data-toggle="collapse" data-parent="#accordion" href="#step-1-collapse" aria-expanded="true" aria-controls="step-1-collapse") { + 'Account details' + } + } + div#step-1-collapse.collapse.show(role="tabpanel" aria-labelledby="step-1-heading" data-parent="#accordion") { + div.card-body { + div.row { + div.col-md-6 { + div.form-group { + label.form-label(for="email") {'Email *'} + input.form-control#email(type="email" value=details.email || '' placeholder="Account email address" required="required" maxlength=256) {} + } + } + } + + input.btn.btn-success#step-1-continue(type="button" value="Continue") {} + p.'mt-3'.mb-0 {'* This field is required.'} + } + } + } + div.card#step-2 { + div.card-header#step-2-heading(role="tab") { + span#step-2-tick(style="display: none;") { + span.icon-color.pr-3 {_out.push(icon_tick)} + } + span#step-2-cross(style="display: none;") { + span.icon-color.pr-3 {_out.push(icon_cross)} + } + span#step-2-spinner(style="display: none;") { + span.icon-color.pr-3 { + div.spinner-border(role="status") { + span.sr-only {'Loading...'} + } + } + } + a.h5.collapsed(data-toggle="collapse" data-parent="#accordion" href="#step-2-collapse" aria-expanded="false" aria-controls="step-2-collapse") { + 'Send verification email' + } + } + div#step-2-collapse.collapse(role="tabpanel" aria-labelledby="step-2-heading" data-parent="#accordion") { + div.card-body { + p#step-2-message {'Please enter account details first.'} + + input.btn.btn-outline-secondary#step-2-back(type="button" value="Back") {} + input.btn.btn-outline-secondary.ml-3#step-2-resend-email(type="button" value="Re-send email") {} + } + } + } + } + }, + // scripts + async _out => { + script(src="/js/api_call.js") {} + + script { + let sign_up_send_verification_email = async (...arguments) => api_call( + '/api/sign_up/send_verification_email.json', + ...arguments + ) + + let step_1 = async () => { + if (!document.getElementById('email').reportValidity()) { + $('#step-1-tick').hide() + $('#step-1-cross').show() + //$('#step-1-spinner').hide() + return false + } + $('#step-1-tick').show() + $('#step-1-cross').hide() + //$('#step-1-spinner').hide() + return true + } + + let step_2 = async () => { + $('#step-2-tick').hide() + $('#step-2-cross').hide() + $('#step-2-spinner').show() + document.getElementById('step-1').scrollIntoView() + + let email + try { + email = document.getElementById('email').value.slice(0, 256).toLowerCase() + await sign_up_send_verification_email(email) + } + catch (error) { + let problem = + error instanceof Problem ? + error : + new Problem( + // title + 'Bad request', + // details + (error.stack || error.message).toString() + // status + 400 + ) + + $('#step-2-tick').hide() + $('#step-2-cross').show() + $('#step-2-spinner').hide() + + document.getElementById('step-2-message').textContent = problem.detail + $('#step-2-collapse').collapse('show') + return false + } + $('#step-2-tick').show() + $('#step-2-cross').hide() + $('#step-2-spinner').hide() + + document.getElementById('step-2-message').textContent = `Verification email has been sent to "${email}". Please check your email for next steps.` + return true + } + + document.addEventListener( + 'DOMContentLoaded', + () => { + document.getElementById('step-1-continue').addEventListener( + 'click', + async () => { + if (await step_1() && await step_2()) + $('#step-2-collapse').collapse('show') + } + ) + + document.getElementById('step-2-back').addEventListener( + 'click', + () => {$('#step-1-collapse').collapse('show')} + ) + + document.getElementById('step-2-resend-email').addEventListener( + 'click', + async () => { + if (await step_2()) + $('#step-3-collapse').collapse('show') + } + ) + } + ) + } + } + ) +} diff --git a/my_account/sign_up/index.html.jst b/my_account/sign_up/index.html.jst index 894787c..4038b2e 100644 --- a/my_account/sign_up/index.html.jst +++ b/my_account/sign_up/index.html.jst @@ -33,7 +33,7 @@ return async env => { p {'Signing up allows you to leave comments on our blog and receive communications from us.'} div.accordion#accordion.mb-5(role="tablist" aria-multiselectable="true") { - div.card { + div.card#step-1 { div.card-header#step-1-heading(role="tab") { span#step-1-tick(style="display: none;") { span.icon-color.pr-3 {_out.push(icon_tick)} @@ -118,7 +118,7 @@ return async env => { } } } - div.card { + div.card#step-2 { div.card-header#step-2-heading(role="tab") { span#step-2-tick(style="display: none;") { span.icon-color.pr-3 {_out.push(icon_tick)} @@ -146,7 +146,7 @@ return async env => { } } } - div.card { + div.card#step-3 { div.card-header#step-3-heading(role="tab") { span#step-3-tick(style="display: none;") { span.icon-color.pr-3 {_out.push(icon_tick)} @@ -170,7 +170,7 @@ return async env => { p#step-3-message {'Please create your account first.'} input.btn.btn-outline-secondary#step-3-back(type="button" value="Back") {} - input.btn.btn-outline-secondary.ml-3#step-3-resend-email(type="button" value="Resend email") {} + input.btn.btn-outline-secondary.ml-3#step-3-resend-email(type="button" value="Re-send email") {} } } } @@ -245,6 +245,8 @@ return async env => { $('#step-2-tick').hide() $('#step-2-cross').hide() $('#step-2-spinner').show() + document.getElementById('step-2').scrollIntoView() + try { step_2_details = coerce_details() await sign_up_create_account( @@ -254,19 +256,30 @@ return async env => { step_2_details ) } - catch (e) { + catch (error) { + let problem = + error instanceof Problem ? + error : + new Problem( + // title + 'Bad request', + // details + (error.stack || error.message).toString() + // status + 400 + ) + $('#step-2-tick').hide() $('#step-2-cross').show() $('#step-2-spinner').hide() - document.getElementById('step-2-message').textContent = e.message + document.getElementById('step-2-message').textContent = problem.detail $('#step-2-collapse').collapse('show') return false } $('#step-2-tick').show() $('#step-2-cross').hide() $('#step-2-spinner').hide() - document.getElementById('step-2-message').textContent = `Your account with email "${step_2_details.email}" has been created.` return true } @@ -275,15 +288,29 @@ return async env => { $('#step-3-tick').hide() $('#step-3-cross').hide() $('#step-3-spinner').show() + document.getElementById('step-3').scrollIntoView() + try { await sign_up_send_verification_email(step_2_details.email) } - catch (e) { + catch (error) { + let problem = + error instanceof Problem ? + error : + new Problem( + // title + 'Bad request', + // details + (error.stack || error.message).toString() + // status + 400 + ) + $('#step-3-tick').hide() $('#step-3-cross').show() $('#step-3-spinner').hide() - document.getElementById('step-3-message').textContent = e.message + document.getElementById('step-3-message').textContent = problem.detail $('#step-3-collapse').collapse('show') return false } diff --git a/my_account/verify_email/index.html.jst b/my_account/verify_email/index.html.jst index dee5ab3..ffb3cca 100644 --- a/my_account/verify_email/index.html.jst +++ b/my_account/verify_email/index.html.jst @@ -29,10 +29,10 @@ return async env => { async _out => { await breadcrumbs(env, _out) - p {'You will need to verify your email address via an email link before you can sign in to your account.'} + p {'You will need to verify your email address via an emailed link before you can sign in to your account.'} div.accordion#accordion.mb-5(role="tablist" aria-multiselectable="true") { - div.card { + div.card#step-1 { div.card-header#step-1-heading(role="tab") { span#step-1-tick(style="display: none;") { span.icon-color.pr-3 {_out.push(icon_tick)} @@ -73,7 +73,7 @@ return async env => { } } } - div.card { + div.card#step-2 { div.card-header#step-2-heading(role="tab") { span#step-2-tick(style="display: none;") { span.icon-color.pr-3 {_out.push(icon_tick)} @@ -94,7 +94,7 @@ return async env => { } div#step-2-collapse.collapse(role="tabpanel" aria-labelledby="step-2-heading" data-parent="#accordion") { div.card-body { - p#step-2-message {'Please enter email link details first.'} + p#step-2-message {'Please enter link details first.'} input.btn.btn-outline-secondary#step-2-back(type="button" value="Back") {} input.btn.btn-outline-secondary.ml-2#step-2-sign-in(type="button" value="Sign in") {} @@ -133,6 +133,8 @@ return async env => { $('#step-2-tick').hide() $('#step-2-cross').hide() $('#step-2-spinner').show() + document.getElementById('step-1').scrollIntoView() + let email try { email = document.getElementById('email').value.slice(0, 256).toLowerCase() @@ -143,12 +145,24 @@ return async env => { document.getElementById('link-code').value.slice(0, 32).toLowerCase() ) } - catch (e) { + catch (error) { + let problem = + error instanceof Problem ? + error : + new Problem( + // title + 'Bad request', + // details + (error.stack || error.message).toString() + // status + 400 + ) + $('#step-2-tick').hide() $('#step-2-cross').show() $('#step-2-spinner').hide() - document.getElementById('step-2-message').textContent = e.message + document.getElementById('step-2-message').textContent = problem.detail $('#step-2-collapse').collapse('show') return false } @@ -175,6 +189,11 @@ return async env => { 'click', () => {$('#step-1-collapse').collapse('show')} ) + + document.getElementById('step-2-sign-in').addEventListener( + 'click', + () => {document.getElementById('sign-in').click()} + ) } ) } diff --git a/search/index.html.jst b/search/index.html.jst index b43beb1..e722cd9 100644 --- a/search/index.html.jst +++ b/search/index.html.jst @@ -28,7 +28,7 @@ return async env => { try { menu = await env.site.get_menu(`${pathname.slice(0, i)}_menu.json`) } - catch (e) { + catch (error) { return pathname // fallback } let dir = pathname.slice(i, j)