)
}
catch (error) {
- if (
- error instanceof Problem &&
- error.title === 'Email not yet verified'
- ) {
+ let problem =
+ error instanceof Problem ?
+ error :
+ new Problem(
+ // title
+ 'Bad request',
+ // details
+ (error.stack || error.message).toString()
+ // status
+ 400
+ )
+
+ if (problem.title === 'Email not yet verified') {
location.href = `/my_account/send_verification_email?email=${encodeURIComponent(email)}`
return
}
- document.getElementById('message-modal-message').textContent = error.message
+ document.getElementById('message-modal-message').textContent = problem.detail
$('#sign-in-modal').modal('hide')
$('#message-modal').modal('show')
return
await sign_out()
}
catch (error) {
- document.getElementById('message-modal-message').textContent = error.message
+ let problem =
+ error instanceof Problem ?
+ error :
+ new Problem(
+ // title
+ 'Bad request',
+ // details
+ (error.stack || error.message).toString()
+ // status
+ 400
+ )
+
+ document.getElementById('message-modal-message').textContent = problem.detail
$('#sign-in-modal').modal('hide')
$('#message-modal').modal('show')
return
--- /dev/null
+let crypto = require('crypto')
+let logjson = (await import('@ndcode/logjson')).default
+let XDate = require('xdate')
+
+return async env => {
+ let globals = await env.site.get_json('/_config/globals.json')
+ let nodemailer_noreply = await env.site.get_nodemailer(
+ '/_config/nodemailer_noreply.json'
+ )
+ let post_request = await _require('/_lib/post_request.jst')
+ let session_cookie = await _require('/_lib/session_cookie.jst')
+ let Problem = await _require('/_lib/Problem.jst')
+
+ post_request(
+ // env
+ env,
+ // endpoint
+ '/api/password_reset.json',
+ // handler
+ async (email, password) => {
+ // coerce and/or validate
+ email = email.slice(0, 256).toLowerCase()
+ password = password.slice(0, 256)
+ if (email.length === 0 || password.length < 8)
+ throw new Problem(
+ 'Bad request',
+ 'Minimum length check failed',
+ 400
+ )
+
+ let transaction = await env.site.database.Transaction()
+ try {
+ // initialize env.session_key, set cookie in env.response
+ await session_cookie(env, transaction)
+
+ let account = await (
+ await (
+ await transaction.get({})
+ ).get('accounts', {})
+ ).get(email)
+ if (account === undefined)
+ throw new Problem(
+ 'Account does not exist',
+ `Please create the account for "${email}" before attempting to reset its password.`
+ 421
+ )
+
+ let link_code = crypto.randomBytes(16).toString('hex')
+ let expires = new XDate()
+ expires.addDays(1)
+ account.set(
+ 'verify_password',
+ transaction.json_to_logjson(
+ {
+ password,
+ link_code,
+ expires: expires.getTime()
+ }
+ )
+ )
+
+ let given_names = await logjson.logjson_to_json(
+ await account.get('given_names', '')
+ )
+ let family_name = await logjson.logjson_to_json(
+ await account.get('family_name', '')
+ )
+ let name =
+ family_name.length ? `${given_names} ${family_name}` : given_names
+
+ await nodemailer_noreply.sendMail(
+ {
+ from: globals.noreply_from,
+ to: `${name} <${email}>`,
+ subject: 'Password reset',
+ text: `Dear ${given_names},
+
+We have received a request to reset the account password for your email address.
+
+If this request is valid, please verify the new password by visiting the below link:
+${globals.site_url}/my_account/verify_password/index.html?email=${encodeURIComponent(email)}&link_code=${encodeURIComponent(link_code)}
+
+The link is valid for 24 hours.
+
+Thanks,
+${globals.noreply_signature}
+`
+ }
+ )
+
+ await transaction.commit()
+ }
+ catch (error) {
+ transaction.rollback()
+ throw error
+ }
+ }
+ )
+}
if (account === undefined)
throw new Problem(
'Account does not exist',
- `Please create the account for "${email}" before attempting to send a verification email.`
+ `Please create the account for "${email}" before attempting to send a email verification link.`
421
)
if (account === undefined)
throw new Problem(
'Account does not exist',
- `Please create the account for "${email}" before attempting to verify the email link.`
+ `Please create the account for "${email}" before attempting to verify the email verification link.`
421
)
)
throw new Problem(
'Link code missing',
- `Link code for email "${email}" does not exist or has expired.`,
+ `Email verification link code for account "${email}" does not exist or has expired.`,
423
)
if (
)
throw new Problem(
'Link code mismatch',
- `Provided link code "${link_code}" does not match expected value.`,
+ `Provided email verification link code "${link_code}" does not match expected value.`,
423
)
--- /dev/null
+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')
+
+ post_request(
+ // env
+ env,
+ // endpoint
+ '/api/verify_password.json',
+ // handler
+ async (email, link_code) => {
+ // coerce and/or validate
+ email = email.slice(0, 256).toLowerCase()
+ link_code = link_code.slice(0, 256).toLowerCase()
+ if (email.length === 0 || link_code.length < 32)
+ 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(email)
+ if (account === undefined)
+ throw new Problem(
+ 'Account does not exist',
+ `Please create the account for "${email}" before attempting to verify the password reset link.`
+ 421
+ )
+
+ let verify_password = await account.get('verify_password')
+ if (
+ verify_password === undefined ||
+ XDate.now() >= await logjson.logjson_to_json(
+ await verify_password.get('expires')
+ )
+ )
+ throw new Problem(
+ 'Link code missing',
+ `Password reset link code for account "${email}" does not exist or has expired.`,
+ 423
+ )
+ if (
+ link_code !== await logjson.logjson_to_json(
+ await verify_password.get('link_code')
+ )
+ )
+ throw new Problem(
+ 'Link code mismatch',
+ `Provided password reset link code "${link_code}" does not match expected value.`,
+ 423
+ )
+
+ await account.delete('verify_password')
+ await account.set('password', await verify_password.get('password'))
+
+ await transaction.commit()
+ }
+ catch (error) {
+ transaction.rollback()
+ throw error
+ }
+ }
+ )
+}
--- /dev/null
+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 {'To reset your password, please enter new details below and we will send you a password reset link.'}
+
+ 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) {}
+ }
+ }
+ div.col-md-6 {
+ div.form-group {
+ label.form-label(for="password") {'Password *'}
+ input.form-control#password(type="password" placeholder="New password" required="required" minlength=8 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 password reset link'
+ }
+ }
+ 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 password_reset = async (...arguments) => api_call(
+ '/api/password_reset.json',
+ ...arguments
+ )
+
+ let step_1 = async () => {
+ if (
+ !document.getElementById('email').reportValidity() ||
+ !document.getElementById('password').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 password_reset(
+ email,
+ document.getElementById('password').value.slice(0, 256)
+ )
+ }
+ 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 = `Password reset link 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')
+ }
+ )
+ }
+ )
+ }
+ }
+ )
+}
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.'}
+ p {'Please verify your email address before signing in. Check your email for next steps, or re-send the email verification link below.'}
div.accordion#accordion.mb-5(role="tablist" aria-multiselectable="true") {
div.card#step-1 {
}
}
a.h5.collapsed(data-toggle="collapse" data-parent="#accordion" href="#step-2-collapse" aria-expanded="false" aria-controls="step-2-collapse") {
- 'Send verification email'
+ 'Send email verification link'
}
}
div#step-2-collapse.collapse(role="tabpanel" aria-labelledby="step-2-heading" data-parent="#accordion") {
script(src="/js/api_call.js") {}
script {
- let sign_up_send_verification_email = async (...arguments) => api_call(
- '/api/sign_up/send_verification_email.json',
+ let sign_up_send_email_verification_link = async (...arguments) => api_call(
+ '/api/sign_up/send_email_verification_link.json',
...arguments
)
let email
try {
email = document.getElementById('email').value.slice(0, 256).toLowerCase()
- await sign_up_send_verification_email(email)
+ await sign_up_send_email_verification_link(email)
}
catch (error) {
let problem =
$('#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.`
+ document.getElementById('step-2-message').textContent = `Email verification link has been sent to "${email}". Please check your email for next steps.`
return true
}
}
}
a.h5.collapsed(data-toggle="collapse" data-parent="#accordion" href="#step-3-collapse" aria-expanded="false" aria-controls="step-3-collapse") {
- 'Send verification email'
+ 'Send email verification link'
}
}
div#step-3-collapse.collapse(role="tabpanel" aria-labelledby="step-3-heading" data-parent="#accordion") {
'/api/sign_up/set_draft.json',
...arguments
)
- let sign_up_send_verification_email = async (...arguments) => api_call(
- '/api/sign_up/send_verification_email.json',
+ let sign_up_send_email_verification_link = async (...arguments) => api_call(
+ '/api/sign_up/send_email_verification_link.json',
...arguments
)
document.getElementById('step-3').scrollIntoView()
try {
- await sign_up_send_verification_email(step_2_details.email)
+ await sign_up_send_email_verification_link(step_2_details.email)
}
catch (error) {
let problem =
$('#step-3-cross').hide()
$('#step-3-spinner').hide()
- document.getElementById('step-3-message').textContent = `Verification email has been sent to "${step_2_details.email}". Please check your email for next steps.`
+ document.getElementById('step-3-message').textContent = `Email verification link has been sent to "${step_2_details.email}". Please check your email for next steps.`
return true
}
--- /dev/null
+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)
+ if (
+ Object.prototype.hasOwnProperty.call(
+ env.parsed_url.query,
+ 'link_code'
+ )
+ )
+ details.link_code =
+ decodeURIComponent(env.parsed_url.query.link_code)
+ console.log('details', JSON.stringify(details))
+
+ await navbar(
+ env,
+ // head
+ async _out => {},
+ // body
+ async _out => {
+ await breadcrumbs(env, _out)
+
+ p {'You will need to verify your new password via an emailed link before you can use it to sign in to your account.'}
+
+ 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") {
+ 'Link 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) {}
+ }
+ }
+ div.col-md-6 {
+ div.form-group {
+ label.form-label(for="link-code") {'Link code *'}
+ input.form-control#link-code(type="text" value=details.link_code || '' placeholder="Type the code from the email link" required="required" minlength=32 maxlength=32) {}
+ }
+ }
+ }
+
+ input.btn.btn-success#step-1-continue(type="button" value="Continue") {}
+ 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") {
+ 'Verify password'
+ }
+ }
+ div#step-2-collapse.collapse(role="tabpanel" aria-labelledby="step-2-heading" data-parent="#accordion") {
+ div.card-body {
+ 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") {}
+ }
+ }
+ }
+ }
+ },
+ // scripts
+ async _out => {
+ script(src="/js/api_call.js") {}
+
+ script {
+ let verify_password = async (...arguments) => api_call(
+ '/api/verify_password.json',
+ ...arguments
+ )
+
+ let step_1 = async () => {
+ if (
+ !document.getElementById('email').reportValidity() ||
+ !document.getElementById('link-code').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 verify_password(
+ // email
+ email,
+ // link_code
+ document.getElementById('link-code').value.slice(0, 32).toLowerCase()
+ )
+ }
+ 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 = `New password for "${email}" has been verified. You can now sign in.`
+ 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-sign-in').addEventListener(
+ 'click',
+ () => {document.getElementById('sign-in').click()}
+ )
+ }
+ )
+ }
+ }
+ )
+}