From 918d21b1b9fd53216609770c8dd23cdf60042cad Mon Sep 17 00:00:00 2001 From: Nick Downing Date: Sat, 15 Jan 2022 17:27:23 +1100 Subject: [PATCH] Add /api/sign_(in|out).json endpoints, uncomment sign in/out logic in navbar and make it work again using new style API calls, move /feedback.jst under /api (not working at the moment, as it expects emailjs and uses old style API calls) --- _lib/navbar.jst | 311 +++++++++---------- _lib/post_request.jst | 4 +- api/errors.json | 1 + feedback.html.jst => api/feedback.html.jst | 8 +- api/sign_in.json.jst | 72 +++++ api/sign_out.json.jst | 33 ++ api/sign_up/create_account.json.jst | 4 +- api/sign_up/get_draft.json.jst | 4 +- api/sign_up/send_verification_email.json.jst | 6 +- api/sign_up/set_draft.json.jst | 4 +- api/sign_up/verify_email.json.jst | 4 +- css/bootstrap/_variables.scss | 4 +- 12 files changed, 275 insertions(+), 180 deletions(-) rename feedback.html.jst => api/feedback.html.jst (92%) create mode 100644 api/sign_in.json.jst create mode 100644 api/sign_out.json.jst diff --git a/_lib/navbar.jst b/_lib/navbar.jst index bf97f48..c6ff55f 100644 --- a/_lib/navbar.jst +++ b/_lib/navbar.jst @@ -57,17 +57,17 @@ return async (env, head, body, scripts) => { } ' ' if (env.signed_in_as !== null) - a#sign-in(href="" hidden="") {'Sign in'} + a#sign-in(href="#" style="display: none;") {'Sign in'} else - a#sign-in(href="") {'Sign in'} + a#sign-in(href="#") {'Sign in'} ' ' if (env.signed_in_as !== null) - a#sign-out(href="") {'Sign out'} + a#sign-out(href="#") {'Sign out'} else - a#sign-out(href="" hidden="") {'Sign out'} + a#sign-out(href="#" style="display: none;") {'Sign out'} } - form/*.form-inline*/(action="/search/index.html") { + form(action="/search/index.html") { div.input-group { input.form-control(name="query" type="text" placeholder="Search" aria-describedby="search-button") {} div.input-group-append { @@ -159,7 +159,7 @@ return async (env, head, body, scripts) => { } ul.navbar-nav.ml-auto { li.nav-item { - a.nav-link#give-feedback(href="") {'Give feedback'} + a.nav-link#give-feedback(href="#") {'Give feedback'} } } } @@ -194,56 +194,54 @@ return async (env, head, body, scripts) => { } // hidden part - //div#sign-in-modal.modal.fade(role="dialog") { - // div.modal-dialog { - // div.modal-content { - // div.modal-header { - // span.h4.modal-title {'Sign in'} - // } - // div.modal-body { - // form#sign-in-form(method="post" action="/sign_in.json" role="form") { - // div.row { - // div.col-md-12 { - // div.form-group { - // label(for="sign-in-form-email") {'Email'} - // input.form-control#sign-in-form-email(type="text" name="email" placeholder="Please enter your email address" required="required" data-error="Email address is required.") {} - // div.help-block.with-errors {} - // } - // } - // } - // div.row { - // div.col-md-12 { - // div.form-group { - // label(for="sign-in-form-password") {'Password'} - // input.form-control#sign-in-form-password(type="password" name="password" required="required" placeholder="Please enter your password" data-error="Password is required.") {} - // div.help-block.with-errors {} - // } - // } - // } - // input.btn.btn-success.btn-send(style="display: none;" type="submit" value="Sign in") {} - // } + div#sign-in-modal.modal.fade(role="dialog") { + div.modal-dialog { + div.modal-content { + div.modal-header { + span.h4.modal-title {'Sign in'} + } + div.modal-body { + form#sign-in-form { + div.row { + div.col-md-12 { + div.form-group { + label.form-label(for="sign-in-form-email") {'Email'} + input.form-control#sign-in-form-email(type="text" name="email" placeholder="Account email address" required="required" maxlength=256) {} + } + } + } + div.row { + div.col-md-12 { + div.form-group { + label.form-label(for="sign-in-form-password") {'Password'} + input.form-control#sign-in-form-password(type="password" name="password" placeholder="Account password" required="required" minlength=8 maxlength=256) {} + } + } + } + input.btn.btn-success.btn-send(style="display: none;" type="submit" value="Sign in") {} + } - // p { - // 'No account yet? ' - // a(href="/my_account/sign_up/index.html") {'Sign up'} - // } + p.mt-2 { + 'No account yet? ' + a(href="/my_account/sign_up/index.html") {'Sign up'} + } - // p { - // 'Forgot password? ' - // a(href="/my_account/password_reset/index.html") {'Password reset'} - // } - // } - // div.modal-footer { - // button.btn.btn-primary(type="submit" form="sign-in-form") { - // 'Sign in' - // } - // button.btn.btn-outline-secondary(type="button" data-dismiss="modal") { - // 'Cancel' - // } - // } - // } - // } - //} + p { + 'Forgot password? ' + a(href="/my_account/password_reset/index.html") {'Password reset'} + } + } + div.modal-footer { + button.btn.btn-outline-secondary(type="button" data-dismiss="modal") { + 'Cancel' + } + button.btn.btn-primary(type="submit" form="sign-in-form") { + 'Sign in' + } + } + } + } + } div#feedback-modal.modal.fade(role="dialog") { div.modal-dialog { @@ -255,7 +253,7 @@ return async (env, head, body, scripts) => { p { 'Did you notice something not quite right, or just want to share your impression of this page?' } - form#feedback-form(method="post" action="/feedback.html" role="form") { + form#feedback-form { div.row { div.col-md-12 { div.form-group { @@ -281,12 +279,12 @@ return async (env, head, body, scripts) => { } } div.modal-footer { - button.btn.btn-primary(type="submit" form="feedback-form") { - 'Submit' - } button.btn.btn-outline-secondary(type="button" data-dismiss="modal") { 'Cancel' } + button.btn.btn-primary(type="submit" form="feedback-form") { + 'Submit' + } } } } @@ -298,8 +296,7 @@ return async (env, head, body, scripts) => { div.modal-header { span.h4.modal-title {'Message'} } - div.modal-body { - p#message-modal-message {} + div.modal-body#message-modal-message { } div.modal-footer { button.btn.btn-outline-secondary(type="button" data-dismiss="modal") { @@ -312,125 +309,116 @@ return async (env, head, body, scripts) => { }, // scripts async _out => { - //script(src="/js/sha256.js") {} + script(src="/js/api_call.js") {} script { - //function get_cookie(name) { - // let entries = document.cookie.split(';'); - // for (let i = 0; i < entries.length; ++i) { - // let fields = entries[i].split('='); - // if (fields[0].trim() === name) - // return decodeURIComponent(fields[1]); - // } - // return undefined; - //} + let sign_in = async (...arguments) => api_call( + '/api/sign_in.json', + ...arguments + ) + let sign_out = async (...arguments) => api_call( + '/api/sign_out.json', + ...arguments + ) - //// this function can be overridden in a further script - //function sign_in_out(status) { - //} + // this function can be overridden in a further script + function sign_in_out(status) { + } - $(document).ready( + document.addEventListener( + 'DOMContentLoaded', () => { - //// sign in form - //$('#sign-in').click( - // () => { - // $('#sign-in-form-email').text('') - // $('#sign-in-form-password').text('') - // $('#sign-in-modal').modal('show') - // return false - // } - //) - //$('#sign-in-modal').on( - // 'shown.bs.modal', - // () => { - // $('#sign-in-form-email').focus() - // } - //) - //// when sign in form is submitted, do not reload the page - //$(document).on( - // 'submit', - // '#sign-in-form', - // e => { - // e.preventDefault() - // $.ajax( - // { - // url: '/my_account/sign_in.json', - // type: 'POST', - // data: { - // email: $('#sign-in-form-email').val(), - // password: sha256( - // get_cookie('session_key') + - // $('#sign-in-form-password').val() - // ).toString('hex') - // }, - // success: (data, textStatus, jqXHR) => { - // $('#sign-in-modal').modal('hide') - // switch (data.result) { - // case 1: // success - // $('#signed-in-status').text(data.signed_in_status) - // $('#sign-in').hide() - // $('#sign-out').show() - // sign_in_out(true) // notify navbar caller - // break - // case 2: // redirect - // location.href = data.redirect_href - // break - // } - // $('#message-modal-message').text(data.message) - // $('#message-modal').modal('show') - // }, - // error: (jqXHR, textStatus, errorThrown) => { - // $('#sign-in-modal').modal('hide') - // $('#message-modal-message').text(errorThrown) - // $('#message-modal').modal('show') - // } - // } - // ) - // } - //) + // sign in form + document.getElementById('sign-in').addEventListener( + 'click', + () => { + document.getElementById('sign-in-form-email').value = '' + document.getElementById('sign-in-form-password').value = '' + $('#sign-in-modal').modal('show') + } + ) - //// sign out button - //$('#sign-out').click( - // () => { - // $.ajax( - // { - // url: '/my_account/sign_out.json', - // type: 'GET', - // success: (data, textStatus, jqXHR) => { - // if (data.result) { - // $('#signed-in-status').text(data.signed_in_status) - // $('#sign-in').show() - // $('#sign-out').hide() - // sign_in_out(false) // notify navbar caller - // } - // $('#message-modal-message').text(data.message) - // $('#message-modal').modal('show') - // }, - // error: (jqXHR, textStatus, errorThrown) => { - // $('#message-modal-message').text(errorThrown) - // $('#message-modal').modal('show') - // } - // } - // ) - // return false - // } - //) + $('#sign-in-modal').on( + 'shown.bs.modal', + () => { + console.log('bloo') + $('#sign-in-form-email').focus() + } + ) + + $(document).on( + 'submit', + '#sign-in-form', + async e => { + e.preventDefault() + let email + try { + email = document.getElementById('sign-in-form-email').value.slice(0, 256).toLowerCase() + await sign_in( + email, + document.getElementById('sign-in-form-password').value.slice(0, 256) + ) + } + catch (error) { + document.getElementById('message-modal-message').textContent = error.message + $('#sign-in-modal').modal('hide') + $('#message-modal').modal('show') + return + } + + document.getElementById('signed-in-status').textContent = `Signed in as ${email}.` + $('#sign-in').hide() + $('#sign-out').show() + sign_in_out(true) + + document.getElementById('message-modal-message').textContent = `You are now signed in as "${email}".` + $('#sign-in-modal').modal('hide') + $('#message-modal').modal('show') + } + ) + + // sign out button + document.getElementById('sign-out').addEventListener( + 'click', + async () => { + try { + await sign_out() + } + catch (error) { + document.getElementById('message-modal-message').textContent = error.message + $('#sign-in-modal').modal('hide') + $('#message-modal').modal('show') + return + } + + document.getElementById('signed-in-status').textContent = 'Browsing as guest.' + $('#sign-in').show() + $('#sign-out').hide() + sign_in_out(false) + + document.getElementById('message-modal-message').textContent = `You are now signed out.` + $('#sign-in-modal').modal('hide') + $('#message-modal').modal('show') + } + ) // feedback form - $('#give-feedback').click( + document.getElementById('give-feedback').addEventListener( + 'click', () => { $('#feedback-form-message').text('') $('#feedback-modal').modal('show') return false } ) + $('#feedback-modal').on( 'shown.bs.modal', () => { $('#feedback-form-message').focus() } ) - // when feedback form is submitted, do not reload the page + $(document).on( 'submit', '#feedback-form', @@ -438,7 +426,7 @@ return async (env, head, body, scripts) => { e.preventDefault() $.ajax( { - url: '/feedback.html', + url: '/api/feedback.html', type: 'POST', data: { page: window.location.href, @@ -446,12 +434,13 @@ return async (env, head, body, scripts) => { }, success: (data, textStatus, jqXHR) => { $('#feedback-modal').modal('hide') + document.getElementById('message-modal-message').textContent = data $('#message-modal-message').text(data) $('#message-modal').modal('show') }, error: (jqXHR, textStatus, errorThrown) => { $('#feedback-modal').modal('hide') - $('#message-modal-message').text(errorThrown) + document.getElementById('message-modal-message').textContent = errorThrown $('#message-modal').modal('show') } } diff --git a/_lib/post_request.jst b/_lib/post_request.jst index 2b968bb..59c780e 100644 --- a/_lib/post_request.jst +++ b/_lib/post_request.jst @@ -1,6 +1,6 @@ let stream_buffers = require('stream-buffers') -return async (env, endpoint, func) => { +return async (env, endpoint, handler) => { let Problem = await _require('/_lib/Problem.jst') let result @@ -26,7 +26,7 @@ return async (env, endpoint, func) => { let arguments = JSON.parse((await data).toString()) console.log('endpoint', endpoint, 'arguments', JSON.stringify(arguments)) - result = await func(...arguments) + result = await handler(...arguments) if (result === undefined) result = null console.log('endpoint', endpoint, 'result', JSON.stringify(result)) diff --git a/api/errors.json b/api/errors.json index 4ac84de..ae3e6dd 100644 --- a/api/errors.json +++ b/api/errors.json @@ -23,6 +23,7 @@ "422": "Email already verified", "423": "Link code missing", "424": "Link code mismatch", + "425": "Email not yet verified", "500": "Internal server error", "501": "Not implemented", "502": "Bad gateway", diff --git a/feedback.html.jst b/api/feedback.html.jst similarity index 92% rename from feedback.html.jst rename to api/feedback.html.jst index b7b68e6..b67836b 100644 --- a/feedback.html.jst +++ b/api/feedback.html.jst @@ -4,6 +4,9 @@ let XDate = require('xdate') return async env => { let globals = await env.site.get_json('/_config/globals.json') + let nodemailer_feedback = await env.site.get_nodemailer( + '/_config/email_feedback.json' + ) let message if (env.request.method === 'POST') { @@ -37,10 +40,7 @@ return async env => { transaction.commit() // send email (asynchronously) - let emailjs_feedback = await env.site.get_emailjs( - '/_config/email_feedback.json' - ) - emailjs_feedback.send( + nodemailer_feedback.sendMail( { from: globals.feedback_from, to: globals.feedback_to, diff --git a/api/sign_in.json.jst b/api/sign_in.json.jst new file mode 100644 index 0000000..30f8564 --- /dev/null +++ b/api/sign_in.json.jst @@ -0,0 +1,72 @@ +let logjson = (await import('@ndcode/logjson')).default + +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/sign_in.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 + let session = await session_cookie(env, transaction) + + let account = await ( + await ( + await transaction.get({}) + ).get('accounts', {}) + ).get(email) + if ( + account === undefined || + password !== await logjson.logjson_to_json( + await account.get('password') + ) + ) + throw new Problem( + 'Unauthorized', + 'Email and password combination was incorrect.' + 401 + ) + + if ( + !await logjson.logjson_to_json( + await account.get('email_verified') + ) + ) + throw new Problem( + 'Email not yet verified', + 'Please verify your email address via email link before trying to sign in.', + 425 + ) + + session.set('signed_in_as', transaction.json_to_logjson(email)) + + await transaction.commit() + } + catch (error) { + transaction.rollback() + throw error + } + } + ) +} diff --git a/api/sign_out.json.jst b/api/sign_out.json.jst new file mode 100644 index 0000000..768de0e --- /dev/null +++ b/api/sign_out.json.jst @@ -0,0 +1,33 @@ +let logjson = (await import('@ndcode/logjson')).default + +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/sign_out.json', + // handler + async () => { + let transaction = await env.site.database.Transaction() + try { + // initialize env.session_key, set cookie in env.response + let session = await session_cookie(env, transaction) + + session.set('signed_in_as', null) + await transaction.commit() + } + catch (error) { + transaction.rollback() + throw error + } + } + ) +} diff --git a/api/sign_up/create_account.json.jst b/api/sign_up/create_account.json.jst index c3c74b6..5789c6a 100644 --- a/api/sign_up/create_account.json.jst +++ b/api/sign_up/create_account.json.jst @@ -9,8 +9,8 @@ return async env => { // env env, // endpoint - '/api/sign_up/create_account', - // func + '/api/sign_up/create_account.json', + // handler async (verification_code, details) => { // coerce and/or validate verification_code = verification_code.slice(0, 6).toLowerCase() diff --git a/api/sign_up/get_draft.json.jst b/api/sign_up/get_draft.json.jst index 6cdd586..6bdb55a 100644 --- a/api/sign_up/get_draft.json.jst +++ b/api/sign_up/get_draft.json.jst @@ -10,8 +10,8 @@ return async env => { // env env, // endpoint - '/api/sign_up/get_draft', - // func + '/api/sign_up/get_draft.json', + // handler async () => { let transaction = await env.site.database.Transaction() try { diff --git a/api/sign_up/send_verification_email.json.jst b/api/sign_up/send_verification_email.json.jst index 7ef9d16..3ab682c 100644 --- a/api/sign_up/send_verification_email.json.jst +++ b/api/sign_up/send_verification_email.json.jst @@ -3,10 +3,10 @@ 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 globals = await env.site.get_json('/_config/globals.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') @@ -15,8 +15,8 @@ return async env => { // env env, // endpoint - '/api/sign_up/send_verification_email', - // func + '/api/sign_up/send_verification_email.json', + // handler async email => { // coerce and/or validate email = email.slice(0, 256).toLowerCase() diff --git a/api/sign_up/set_draft.json.jst b/api/sign_up/set_draft.json.jst index de69d3a..3b3c71b 100644 --- a/api/sign_up/set_draft.json.jst +++ b/api/sign_up/set_draft.json.jst @@ -9,8 +9,8 @@ return async env => { // env env, // endpoint - '/api/sign_up/set_draft', - // func + '/api/sign_up/set_draft.json', + // handler async details => { // coerce and/or validate details = { diff --git a/api/sign_up/verify_email.json.jst b/api/sign_up/verify_email.json.jst index b82e328..dc70f77 100644 --- a/api/sign_up/verify_email.json.jst +++ b/api/sign_up/verify_email.json.jst @@ -11,8 +11,8 @@ return async env => { // env env, // endpoint - '/api/sign_up/verify_email', - // func + '/api/sign_up/verify_email.json', + // handler async (email, link_code) => { // coerce and/or validate email = email.slice(0, 256).toLowerCase() diff --git a/css/bootstrap/_variables.scss b/css/bootstrap/_variables.scss index 7b81e0f..5b37add 100644 --- a/css/bootstrap/_variables.scss +++ b/css/bootstrap/_variables.scss @@ -965,7 +965,7 @@ $modal-content-color: null !default; $modal-content-bg: $white !default; $modal-content-border-color: rgba($black, .2) !default; $modal-content-border-width: $border-width !default; -$modal-content-border-radius: $border-radius-lg !default; +$modal-content-border-radius: 0 !default; //$border-radius-lg !default; $modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default; $modal-content-box-shadow-xs: 0 .25rem .5rem rgba($black, .5) !default; $modal-content-box-shadow-sm-up: 0 .5rem 1rem rgba($black, .5) !default; @@ -976,7 +976,7 @@ $modal-header-border-color: $border-color !default; $modal-footer-border-color: $modal-header-border-color !default; $modal-header-border-width: $modal-content-border-width !default; $modal-footer-border-width: $modal-header-border-width !default; -$modal-header-padding-y: 1rem !default; +$modal-header-padding-y: 0.5rem !default; //1rem !default; $modal-header-padding-x: 1rem !default; $modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility -- 2.34.1