From a9a78632fb4525d9916a993352c1e7e3e5b87600 Mon Sep 17 00:00:00 2001 From: Shaun Warman Date: Wed, 22 Apr 2020 23:59:10 -0500 Subject: [PATCH] chore: revamp otp --- template/app/controllers/web/2fa/index.js | 3 +- template/app/controllers/web/2fa/otp.js | 130 ++++++++++++++ template/app/controllers/web/my-account.js | 27 --- template/app/views/2fa/otp/keys.pug | 41 +++++ .../2fa/{otp-login.pug => otp/login.pug} | 2 +- .../{otp-recovery.pug => otp/recovery.pug} | 2 +- template/app/views/2fa/otp/verify.pug | 108 ++++++++++++ template/app/views/my-account/security.pug | 162 ++---------------- template/config/phrases.js | 1 + template/package.json | 6 +- template/routes/web/2fa.js | 9 +- template/routes/web/my-account.js | 2 +- template/yarn.lock | 42 ++--- 13 files changed, 334 insertions(+), 201 deletions(-) create mode 100644 template/app/controllers/web/2fa/otp.js create mode 100644 template/app/views/2fa/otp/keys.pug rename template/app/views/2fa/{otp-login.pug => otp/login.pug} (99%) rename template/app/views/2fa/{otp-recovery.pug => otp/recovery.pug} (97%) create mode 100644 template/app/views/2fa/otp/verify.pug diff --git a/template/app/controllers/web/2fa/index.js b/template/app/controllers/web/2fa/index.js index 19d4560c..db278cb4 100644 --- a/template/app/controllers/web/2fa/index.js +++ b/template/app/controllers/web/2fa/index.js @@ -1,3 +1,4 @@ +const otp = require('./otp'); const recovery = require('./recovery'); -module.exports = { recovery }; +module.exports = { otp, recovery }; diff --git a/template/app/controllers/web/2fa/otp.js b/template/app/controllers/web/2fa/otp.js new file mode 100644 index 00000000..40e55630 --- /dev/null +++ b/template/app/controllers/web/2fa/otp.js @@ -0,0 +1,130 @@ +const cryptoRandomString = require('crypto-random-string'); +const isSANB = require('is-string-and-not-blank'); +const qrcode = require('qrcode'); +const Boom = require('@hapi/boom'); +const { authenticator } = require('otplib'); +const config = require('../../../../config'); + +async function keys(ctx) { + const { body } = ctx.request; + + if (!isSANB(body.password)) + throw Boom.badRequest(ctx.translate('INVALID_PASSWORD')); + + const { user } = await ctx.state.user.authenticate(body.password); + if (!user) throw Boom.badRequest(ctx.translate('INVALID_PASSWORD')); + + const redirectTo = `/${ctx.locale}/2fa/otp/verify`; + const message = ctx.translate('PASSWORD_CONFIRM_SUCCESS'); + if (ctx.accepts('html')) { + ctx.flash('success', message); + ctx.redirect(redirectTo); + } else { + ctx.body = { + message, + redirectTo + }; + } +} + +async function renderKeys(ctx) { + ctx.state.user[ + config.passport.fields.twoFactorToken + ] = authenticator.generateSecret(); + + // generate 2fa recovery keys list used for fallback + const recoveryKeys = new Array(16) + .fill() + .map(() => cryptoRandomString({ length: 10, characters: '1234567890' })); + + ctx.state.user[config.userFields.twoFactorRecoveryKeys] = recoveryKeys; + ctx.state.user = await ctx.state.user.save(); + + await ctx.render('2fa/otp/keys'); +} + +async function renderVerify(ctx) { + ctx.state.twoFactorTokenURI = authenticator.keyuri( + ctx.state.user.email, + process.env.WEB_HOST, + ctx.state.user[config.passport.fields.twoFactorToken] + ); + ctx.state.qrcode = await qrcode.toDataURL(ctx.state.twoFactorTokenURI); + + await ctx.render('2fa/otp/verify'); +} + +async function disable(ctx) { + const { body } = ctx.request; + + const redirectTo = `/${ctx.locale}/my-account/security`; + + if (!isSANB(body.password)) + throw Boom.badRequest(ctx.translate('INVALID_PASSWORD')); + + const { user } = await ctx.state.user.authenticate(body.password); + if (!user) throw Boom.badRequest(ctx.translate('INVALID_PASSWORD')); + + ctx.state.user[config.passport.fields.twoFactorEnabled] = false; + await ctx.state.user.save(); + + ctx.flash('custom', { + title: ctx.request.t('Success'), + text: ctx.translate('REQUEST_OK'), + type: 'success', + toast: true, + showConfirmButton: false, + timer: 3000, + position: 'top' + }); + + if (ctx.accepts('html')) ctx.redirect(redirectTo); + else ctx.body = { redirectTo }; +} + +async function verify(ctx) { + const redirectTo = `/${ctx.locale}/my-account/security`; + if (ctx.method === 'DELETE') { + ctx.state.user[config.passport.fields.twoFactorEnabled] = false; + } else if ( + ctx.method === 'POST' && + ctx.state.user[config.passport.fields.twoFactorToken] + ) { + const isValid = authenticator.verify({ + token: ctx.request.body.token, + secret: ctx.state.user[config.passport.fields.twoFactorToken] + }); + + if (!isValid) + return ctx.throw(Boom.badRequest(ctx.translate('INVALID_OTP_PASSCODE'))); + + ctx.state.user[config.passport.fields.twoFactorEnabled] = true; + } else { + return ctx.throw(Boom.badRequest('Invalid method')); + } + + await ctx.state.user.save(); + + ctx.session.otp = 'otp-setup'; + + ctx.flash('custom', { + title: ctx.request.t('Success'), + text: ctx.translate('REQUEST_OK'), + type: 'success', + toast: true, + showConfirmButton: false, + timer: 3000, + position: 'top' + }); + + if (ctx.accepts('html')) ctx.redirect(redirectTo); + else ctx.body = { redirectTo }; +} + +module.exports = { + disable, + keys, + renderKeys, + renderVerify, + verify +}; diff --git a/template/app/controllers/web/my-account.js b/template/app/controllers/web/my-account.js index 8509145d..1d682256 100644 --- a/template/app/controllers/web/my-account.js +++ b/template/app/controllers/web/my-account.js @@ -1,8 +1,6 @@ const Boom = require('@hapi/boom'); -const cryptoRandomString = require('crypto-random-string'); const humanize = require('humanize-string'); const isSANB = require('is-string-and-not-blank'); -const qrcode = require('qrcode'); const { authenticator } = require('otplib'); const { boolean } = require('boolean'); @@ -78,30 +76,6 @@ async function resetAPIToken(ctx) { else ctx.body = { reloadPage: true }; } -async function security(ctx) { - if (!ctx.state.user[config.passport.fields.twoFactorEnabled]) { - ctx.state.user[ - config.passport.fields.twoFactorToken - ] = authenticator.generateSecret(); - - // generate 2fa recovery keys list used for fallback - const recoveryKeys = new Array(16) - .fill() - .map(() => cryptoRandomString({ length: 10, characters: '1234567890' })); - - ctx.state.user[config.userFields.twoFactorRecoveryKeys] = recoveryKeys; - ctx.state.user = await ctx.state.user.save(); - ctx.state.twoFactorTokenURI = authenticator.keyuri( - ctx.state.user.email, - process.env.WEB_HOST, - ctx.state.user[config.passport.fields.twoFactorToken] - ); - ctx.state.qrcode = await qrcode.toDataURL(ctx.state.twoFactorTokenURI); - } - - await ctx.render('my-account/security'); -} - async function recoveryKeys(ctx) { const twoFactorRecoveryKeys = ctx.state.user[config.userFields.twoFactorRecoveryKeys]; @@ -158,6 +132,5 @@ module.exports = { update, recoveryKeys, resetAPIToken, - security, setup2fa }; diff --git a/template/app/views/2fa/otp/keys.pug b/template/app/views/2fa/otp/keys.pug new file mode 100644 index 00000000..f87b7cef --- /dev/null +++ b/template/app/views/2fa/otp/keys.pug @@ -0,0 +1,41 @@ + +extends ../../layout + +block body + .container.py-3 + .row + .col-xs-12.col-sm-12.col-md-6.offset-md-3.col-lg-6.offset-lg-3 + .card + .card-body + .text-center + h1.card-title.h4= t('OTP Recovery Keys') + p= t('Recovery keys allow you to login to your account when you have lost access to your Two-Factor Authentication device or authenticator app.') + hr + p.text-muted.font-weight-bold= t('Backup your recovery keys before continuing') + .container + .row + label(for="two-factor-recovery-keys")= t('Recovery keys') + textarea(rows='4').form-control#two-factor-recovery-keys + = user[config.userFields.twoFactorRecoveryKeys].join('\n') + .row.mt-3.mb-3 + .col-sm + form(action=l('/my-account/recovery-keys'), method='POST') + input(type="hidden", name="_csrf", value=ctx.csrf) + button(type='submit').btn.btn-primary.btn-block + i.fa.fa-download + = ' ' + = t('Download') + .col-sm.offset-sm-1 + button(type='button', data-toggle="clipboard", data-clipboard-target="#two-factor-recovery-keys").btn.btn-secondary.btn-block + i.fa.fa-clipboard + = ' ' + = t('Copy') + form.ajax-form.confirm-prompt(action=ctx.path, method="POST", autocomplete="off") + hr + .row + form(action=l('/2fa/otp/keys'), method='POST').col-md-12 + input(type="hidden", name="_csrf", value=ctx.csrf) + .form-group.floating-label + input#input-password.form-control(type="password", autocomplete="off", name="password" required) + label(for="input-password")= t('Confirm Password') + button.btn.btn-primary.btn-md.btn-block.mt-2(type="submit")= t('Continue') diff --git a/template/app/views/2fa/otp-login.pug b/template/app/views/2fa/otp/login.pug similarity index 99% rename from template/app/views/2fa/otp-login.pug rename to template/app/views/2fa/otp/login.pug index 3e6b2492..da019504 100644 --- a/template/app/views/2fa/otp-login.pug +++ b/template/app/views/2fa/otp/login.pug @@ -1,5 +1,5 @@ -extends ../layout +extends ../../layout block body .container.py-3 diff --git a/template/app/views/2fa/otp-recovery.pug b/template/app/views/2fa/otp/recovery.pug similarity index 97% rename from template/app/views/2fa/otp-recovery.pug rename to template/app/views/2fa/otp/recovery.pug index 8f0f35aa..63631cab 100644 --- a/template/app/views/2fa/otp-recovery.pug +++ b/template/app/views/2fa/otp/recovery.pug @@ -1,5 +1,5 @@ -extends ../layout +extends ../../layout block body .container.py-3 diff --git a/template/app/views/2fa/otp/verify.pug b/template/app/views/2fa/otp/verify.pug new file mode 100644 index 00000000..d0cefd8c --- /dev/null +++ b/template/app/views/2fa/otp/verify.pug @@ -0,0 +1,108 @@ + +extends ../../layout + +block body + #authenticator-apps-modal(tabindex='-1', role='dialog').modal.fade + .modal-dialog(role='document') + .modal-content + .modal-header.d-block.text-center + h6.modal-title.d-inline-block.ml-4= t('Authentication Apps') + button(type='button', data-dismiss='modal', aria-label='Close').close + span(aria-hidden='true') × + .modal-body.text-center + = t('Recommendations are listed below:') + .flex-wrap.flex-fill.text-center + = t('Free and Open-Source Software:') + ul.list-group.text-center.mb-3 + li.list-group-item.border-0 + a(href='https://freeotp.github.io/', rel='noopener', target='_blank') FreeOTP + ul.list-inline + li.list-inline-item + a(href='https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary + i.fab.fa-google-play + = ' ' + = t('Google Play') + li.list-inline-item + a(href='https://itunes.apple.com/us/app/freeotp-authenticator/id872559395?mt=8', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary + i.fab.fa-app-store-ios + = ' ' + = t('App Store') + li.list-group-item.border-0 + a(href='https://f-droid.org/en/packages/org.shadowice.flocke.andotp', rel='noopener', target='_blank') andOTP + ul.list-inline + li.list-inline-item + a(href='https://f-droid.org/repo/org.shadowice.flocke.andotp_28.apk', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary + i.fab.fa-google-play + = ' ' + = t('Google Play') + = t('Closed-Source Software:') + ul.list-group.text-center + li.list-group-item.border-0 + a(href='https://authy.com/', rel='noopener', target='_blank') Authy + ul.list-inline + li.list-inline-item + a(href='https://play.google.com/store/apps/details?id=com.authy.authy', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary + i.fab.fa-google-play + = ' ' + = t('Google Play') + li.list-inline-item + a(href='https://itunes.apple.com/us/app/authy/id494168017', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary + i.fab.fa-app-store-ios + = ' ' + = t('App Store') + li.list-group-item.border-0.mb-4 + a(href='https://support.google.com/accounts/answer/1066447', rel='noopener', target='_blank') Google Authenticator + ul.list-inline + li.list-inline-item + a(href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary + i.fab.fa-google-play + = ' ' + = t('Google Play') + li.list-inline-item + a(href='http://appstore.com/googleauthenticator', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary + i.fab.fa-app-store-ios + = ' ' + = t('App Store') + .container.py-3 + .row + .col-xs-12.col-sm-12.col-md-6.offset-md-3.col-lg-6.offset-lg-3 + .card + .card-body + .text-center + h1.card-title.h4= t('Enable OTP') + p= t('Follow the below steps to enable two-factor authentication on your account.') + hr + .container + form(action=l('/2fa/otp/verify'), method='POST').ajax-form.confirm-prompt + input(type="hidden", name="_csrf", value=ctx.csrf) + label(for='two-factor-step-one') + b= t('Step 1: ') + = t('Install an') + = ' ' + a.card-link(href='#' data-toggle='modal-anchor', data-target='#authenticator-apps-modal').text-primary= t('authentication app') + = ' ' + = t('on your device.') + label(for='two-factor-step-two') + b= t('Step 2: ') + = t('Scan this QR code using the app:') + img(src=qrcode, width=250, height=250, alt="").mx-auto.d-block + hr + label(for='two-factor-step-three') + b= t('Step 3: ') + = t('Enter the token generated from the app:') + .form-group.floating-label + input(type='text', name='token', required, placeholder=' ').form-control.form-control-lg#input-token + label(for='input-token') Verification Token + a.card-link.text-primary(href='#' data-toggle='collapse' data-target='#two-factor-copy')= t('Can’t scan the QR code? Follow alternative steps') + #two-factor-copy.collapse + hr + p.text-secondary= t('Manually configure your authenticator app using this code:') + .input-group.input-group-sm.floating-label.form-group + input(type='text', readonly, value=user[config.passport.fields.twoFactorToken]).form-control#two-factor-token + .input-group-append + button(type='button', data-toggle="clipboard", data-clipboard-target="#two-factor-token").btn.btn-primary + i.fa.fa-clipboard + = ' ' + = t('Copy') + hr + button(type='submit').btn.btn-lg.btn-block.btn-primary= t('Enable OTP') diff --git a/template/app/views/my-account/security.pug b/template/app/views/my-account/security.pug index 31212a21..e0ed164c 100644 --- a/template/app/views/my-account/security.pug +++ b/template/app/views/my-account/security.pug @@ -2,147 +2,22 @@ extends ../layout block body - if !user[config.passport.fields.twoFactorEnabled] - #modal-enable-2fa(tabindex='-1', role='dialog').modal.fade - .modal-dialog(role='document') - .modal-content - .modal-header.d-block.text-center - h5.modal-title.d-inline-block.ml-4= t('Two-Factor Recovery Keys') - button(type='button', data-dismiss='modal', aria-label='Close').close - span(aria-hidden='true') × - .modal-body - p= t('Recovery keys allow you to login to your account when you have lost access to your Two-Factor Authentication device or authenticator app.') - hr - p.text-muted.font-weight-bold= t('Backup your recovery keys before continuing') - .container - .row - label(for="two-factor-recovery-keys")= t('Recovery keys') - textarea(rows='4').form-control#two-factor-recovery-keys - = user[config.userFields.twoFactorRecoveryKeys].join('\n') - hr - .row - .col-sm - form(action=l('/my-account/recovery-keys'), method='POST') - input(type="hidden", name="_csrf", value=ctx.csrf) - button(type='submit').btn.btn-primary.btn-block - i.fa.fa-download - = ' ' - = t('Download') - .col-sm.offset-sm-1 - button(type='button', data-toggle="clipboard", data-clipboard-target="#two-factor-recovery-keys").btn.btn-secondary.btn-block - i.fa.fa-clipboard - = ' ' - = t('Copy') - .modal-footer - .form-check - input#recovery-keys-stored(type='checkbox', name="recovery-keys-stored", value='true' data-toggle='collapse' data-target='#recovery-key-submit').form-check-input - label(for='recovery-keys-stored').form-check-label= t('I have stored these recovery keys in a safe place') - #recovery-key-submit.collapse - button(data-toggle='modal', data-target='#modal-enable-2fa-verify', data-dismiss='modal', type='button').btn.btn-primary.btn-lg.float-right= t('Continue') - #modal-enable-2fa-verify(tabindex='-1', role='dialog').modal.fade - .modal-dialog(role='document') - .modal-content - .modal-header.d-block.text-center - h5.modal-title.d-inline-block.ml-4= t('Enable Two-Factor Authentication') - button(type='button', data-dismiss='modal', aria-label='Close').close - span(aria-hidden='true') × - .modal-body - form(action=l('/my-account/setup-2fa'), method='POST').ajax-form.confirm-prompt + #modal-disable-2fa(tabindex='-1', role='dialog').modal.fade + .modal-dialog(role='document') + .modal-content + .modal-header.d-block.text-center + h6.modal-title.d-inline-block.ml-4= t('Disable OTP') + button(type='button', data-dismiss='modal', aria-label='Close').close + span(aria-hidden='true') × + .modal-body.text-center + = t('You are about to disable two-factor authentication on your account. Please confirm your password to continue.') + .row + form(action=l('/2fa/otp/disable'), method='POST').col-md-12 input(type="hidden", name="_csrf", value=ctx.csrf) - label(for='two-factor-step-one') - b= t('Step 1: ') - = t('Install an') - = ' ' - a.card-link(data-toggle='modal-anchor', data-target='#authenticator-apps-modal').text-primary= t('authentication app') - = ' ' - = t('on your device.') - label(for='two-factor-step-two') - b= t('Step 2: ') - = t('Scan this QR code using the app:') - img(src=qrcode, width=250, height=250, alt="").mx-auto.d-block - hr - label(for='two-factor-step-three') - b= t('Step 3: ') - = t('Enter the token generated from the app:') .form-group.floating-label - input(type='text', name='token', required, placeholder=' ').form-control.form-control-lg#input-token - label(for='input-token') Verification Token - a.card-link.text-primary(data-toggle='collapse' data-target='#two-factor-token')= t('Can’t scan the QR code? Follow alternative steps') - #two-factor-token.collapse - hr - p.text-secondary= t('Scan the following QR code in your authenticator app.') - .input-group.input-group-sm.floating-label.form-group - input(type='text', readonly, value=user[config.passport.fields.twoFactorToken]).form-control#two-factor-token - .input-group-append - button(type='button', data-toggle="clipboard", data-clipboard-target="#two-factor-token").btn.btn-primary - i.fa.fa-clipboard - = ' ' - = t('Copy') - hr - button(type='submit').btn.btn-lg.btn-block.btn-primary= t('Continue') - #authenticator-apps-modal(tabindex='-1', role='dialog').modal.fade - .modal-dialog(role='document') - .modal-content - .modal-header.d-block.text-center - h6.modal-title.d-inline-block.ml-4= t('Authentication Apps') - button(type='button', data-dismiss='modal', aria-label='Close').close - span(aria-hidden='true') × - .modal-body.text-center - = t('Recommendations are listed below:') - .flex-wrap.flex-fill.text-center - hr - = t('Free and Open-Source Software:') - ul.list-group.text-center - li.list-group-item.border-0 - a(href='https://freeotp.github.io/', rel='noopener', target='_blank') FreeOTP - ul.list-line - li.list-inline-item - a(href='https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary - i.fab.fa-google-play - = ' ' - = t('Google Play') - li.list-inline-item - a(href='https://itunes.apple.com/us/app/freeotp-authenticator/id872559395?mt=8', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary - i.fab.fa-app-store-ios - = ' ' - = t('App Store') - li.list-group-item.border-0 - a(href='https://f-droid.org/en/packages/org.shadowice.flocke.andotp', rel='noopener', target='_blank') andOTP - ul.list-inline - li.list-inline-item - a(href='https://f-droid.org/repo/org.shadowice.flocke.andotp_28.apk', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary - i.fab.fa-google-play - = ' ' - = t('Google Play') - hr - = t('Closed-Source Software:') - ul.list-group.text-center - li.list-group-item.border-0 - a(href='https://authy.com/', rel='noopener', target='_blank') Authy - ul.list-inline - li.list-inline-item - a(href='https://play.google.com/store/apps/details?id=com.authy.authy', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary - i.fab.fa-google-play - = ' ' - = t('Google Play') - li.list-inline-item - a(href='https://itunes.apple.com/us/app/authy/id494168017', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary - i.fab.fa-app-store-ios - = ' ' - = t('App Store') - li.list-group-item.border-0.mb-2 - a(href='https://support.google.com/accounts/answer/1066447', rel='noopener', target='_blank') Google Authenticator - ul.list-inline - li.list-inline-item - a(href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary - i.fab.fa-google-play - = ' ' - = t('Google Play') - li.list-inline-item - a(href='http://appstore.com/googleauthenticator', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary - i.fab.fa-app-store-ios - = ' ' - = t('App Store') + input#input-password.form-control.mt-3(type="password", autocomplete="off", name="password" required) + label(for="input-password")= t('Confirm Password') + button.btn.btn-danger.btn-md.btn-block.mt-2(type="submit")= t('Disable OTP') .container-fluid.py-3 .row.mt-1 .col @@ -157,12 +32,11 @@ block body form(action=l('/my-account/recovery-keys'), method='POST') input(type="hidden", name="_csrf", value=ctx.csrf) button(type='submit').btn.btn-secondary.btn-lg= t('Download recovery keys') - form(action=l('/my-account/setup-2fa'), method='POST').ajax-form.confirm-prompt.mt-3 - input(type='hidden', name='_method', value='DELETE') - input(type="hidden", name="_csrf", value=ctx.csrf) - button(Type='submit').btn.btn-danger.btn-lg= t('Disable Two-Factor Authentication') + button(data-toggle='modal', data-target='#modal-disable-2fa', type='button').btn.btn-danger.btn-lg.mt-3= t('Disable OTP') else - button(data-toggle='modal', data-target='#modal-enable-2fa', type='button').btn.btn-primary.btn-lg= t('Enable Two-Factor Authentication') + h5= t('OTP') + p= t('OTP or one-time password allows you to add Two-Factor Authentication to your account using a device or authenticator app.') + a.btn.btn-primary.btn-lg(href=l('/2fa/otp/keys'), role="button", data-toggle='modal-anchor', data-target='#modal-sign-up')= t('Enable OTP') .card.card-bg-light h4.card-header= t('API Credentials') .card-body diff --git a/template/config/phrases.js b/template/config/phrases.js index 61cde666..bf1590ac 100644 --- a/template/config/phrases.js +++ b/template/config/phrases.js @@ -37,6 +37,7 @@ module.exports = { 'Please log in with two-factor authentication to view the page you requested.', LOGIN_REQUIRED: 'Please log in to view the page you requested.', LOGOUT_REQUIRED: 'Please log out to view the page you requested.', + PASSWORD_CONFIRM_SUCCESS: 'Password successfully confirmed.', PASSWORD_RESET_LIMIT: 'You can only request a password reset every 30 minutes. Please try again %s.', PASSWORD_RESET_SENT: diff --git a/template/package.json b/template/package.json index 7682bc8c..a009e3d5 100644 --- a/template/package.json +++ b/template/package.json @@ -33,7 +33,7 @@ "@fortawesome/fontawesome-free": "^5.13.0", "@hapi/boom": "^9.1.0", "@koa/router": "^8.0.8", - "@ladjs/api": "^0.3.4", + "@ladjs/api": "^0.3.5", "@ladjs/assets": "^0.0.22", "@ladjs/bull": "^1.0.4", "@ladjs/env": "^1.0.0", @@ -42,10 +42,10 @@ "@ladjs/mongoose": "^2.0.1", "@ladjs/mongoose-error-messages": "^0.0.3", "@ladjs/passport": "^1.0.0", - "@ladjs/policies": "^2.0.2", + "@ladjs/policies": "^2.0.4", "@ladjs/proxy": "^1.0.3", "@ladjs/store-ip-address": "^0.0.7", - "@ladjs/web": "^0.5.6", + "@ladjs/web": "^0.5.7", "@primer/css": "^14.3.0", "@tkrotoff/bootstrap-floating-label": "^0.5.1", "accounting": "^0.4.1", diff --git a/template/routes/web/2fa.js b/template/routes/web/2fa.js index 0c1e3808..83d50183 100644 --- a/template/routes/web/2fa.js +++ b/template/routes/web/2fa.js @@ -8,12 +8,17 @@ const router = new Router({ prefix: '/2fa' }); router.use(policies.ensureLoggedIn); router - .get('/otp/login', render('2fa/otp-login')) + .get('/otp/login', render('2fa/otp/login')) .post('/otp/login', web.auth.loginOtp) + .get('/otp/keys', web.twoFactor.otp.renderKeys) + .post('/otp/keys', web.twoFactor.otp.keys) + .get('/otp/verify', web.twoFactor.otp.renderVerify) + .post('/otp/verify', web.twoFactor.otp.verify) + .post('/otp/disable', web.twoFactor.otp.disable) .post('/recovery', web.twoFactor.recovery.recover) .get('/recovery/verify', render('2fa/verify')) .post('/recovery/verify', web.twoFactor.recovery.verify) - .get('/recovery/keys', render('2fa/otp-recovery')) + .get('/recovery/keys', render('2fa/otp/recovery')) .post('/recovery/keys', web.auth.recoveryKey); module.exports = router; diff --git a/template/routes/web/my-account.js b/template/routes/web/my-account.js index 5f13a4a4..c97d485b 100644 --- a/template/routes/web/my-account.js +++ b/template/routes/web/my-account.js @@ -12,7 +12,7 @@ router.use(web.breadcrumbs); router.get('/', render('my-account')); router.put('/', web.myAccount.update); router.delete('/security', web.myAccount.resetAPIToken); -router.get('/security', web.myAccount.security); +router.get('/security', render('my-account/security')); router.post('/setup-2fa', web.myAccount.setup2fa); router.delete('/setup-2fa', web.myAccount.setup2fa); router.post('/recovery-keys', web.myAccount.recoveryKeys); diff --git a/template/yarn.lock b/template/yarn.lock index b4977606..22bb2da5 100644 --- a/template/yarn.lock +++ b/template/yarn.lock @@ -1031,7 +1031,7 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@koa/router@^8.0.5", "@koa/router@^8.0.8": +"@koa/router@8.x", "@koa/router@^8.0.8": version "8.0.8" resolved "https://registry.yarnpkg.com/@koa/router/-/router-8.0.8.tgz#95f32d11373d03d89dcb63fabe9ac6f471095236" integrity sha512-FnT93N4NUehnXr+juupDmG2yfi0JnWdCmNEuIXpCG4TtG+9xvtrLambBH3RclycopVUOEYAim2lydiNBI7IRVg== @@ -1043,12 +1043,12 @@ path-to-regexp "1.x" urijs "^1.19.2" -"@ladjs/api@^0.3.4": - version "0.3.4" - resolved "https://registry.yarnpkg.com/@ladjs/api/-/api-0.3.4.tgz#a8cd0088962c92cba5a8df9c8db50a62cda6b090" - integrity sha512-GD37Wx0Q6tePRkB6egRFe0Mw44IzsTIq6pOR9DfUc+KW+NjBuhJlXQPnQiZb3yxGxa8REVq8Fv6JV8nuZJxfHA== +"@ladjs/api@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@ladjs/api/-/api-0.3.5.tgz#a8b602bc35072a983fd0b7d3b541b4a6b031f44f" + integrity sha512-eoRI3VhyIad4Py9mT9miLD2/fFAcmnvwVMobYRI77rbMWZSbhh13xTsFPwFBD9o5jPPdLpldbQ7koL0FTpAGng== dependencies: - "@koa/router" "^8.0.5" + "@koa/router" "8.x" "@ladjs/i18n" "^3.0.4" "@ladjs/redis" "^1.0.3" "@ladjs/shared-config" "^1.0.6" @@ -1063,7 +1063,7 @@ koa-better-error-handler "^3.0.5" koa-better-timeout "^0.0.4" koa-bodyparser "^4.3.0" - koa-compress "^3.0.0" + koa-compress "^3.1.0" koa-conditional-get "^2.0.0" koa-connect "^2.0.1" koa-etag "^3.0.0" @@ -1179,10 +1179,10 @@ passport-otp-strategy "^1.0.1" validator "^12.1.0" -"@ladjs/policies@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@ladjs/policies/-/policies-2.0.2.tgz#bb4ce97af1b2940cc79359cf3eb93c7c355bdc81" - integrity sha512-+TMGi1LxvZPLa15UvXwnRV964vTIO+1FgGdQtValVL1+BntZA92Ny4mZ9ZDPgiIop+Q/3Stnmqt7imxvA67ERg== +"@ladjs/policies@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@ladjs/policies/-/policies-2.0.4.tgz#257e2ed3c2748821ad9986664da9fa4dbbb7f010" + integrity sha512-+/VbXovsUx82IR7dF2Hl2AGWWYvcKUjxoCd8yMNTEY7CSdjy7mwnVjabDpAu4fwPPuLodyVoFohafpFb00f4jA== dependencies: "@hapi/boom" "^9.0.0" basic-auth "^2.0.1" @@ -1235,13 +1235,13 @@ debug "^4.1.1" validator "^12.1.0" -"@ladjs/web@^0.5.6": - version "0.5.6" - resolved "https://registry.yarnpkg.com/@ladjs/web/-/web-0.5.6.tgz#0f03398c3525ec22b588cbaadca6bc24344a552d" - integrity sha512-Ijc+722pphsX5Y5L9cswLcb6IFsWf21bGZN9wUJeikjkgi7D05sqcYM/nzmbYSSLHsXXPzzC3eFpKdMCoz9maw== +"@ladjs/web@^0.5.7": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@ladjs/web/-/web-0.5.7.tgz#e3f1e2db9d71e6fc06c706feb204c3b2aeebb656" + integrity sha512-qYP/sC4keOjsTMARSbJS3s5MijF3vn4EYzCl/wDQH+bSOfBZbCn3IrQoVqPYlTpb/6CDl5nQ+6C3OBE7faltfg== dependencies: "@hapi/boom" "^9.1.0" - "@koa/router" "^8.0.5" + "@koa/router" "8.x" "@ladjs/i18n" "^3.0.4" "@ladjs/koa-better-static" "^2.0.0" "@ladjs/redis" "^1.0.3" @@ -1259,7 +1259,7 @@ koa-better-flash "^0.0.4" koa-better-timeout "^0.0.4" koa-bodyparser "^4.3.0" - koa-compress "^3.0.0" + koa-compress "^3.1.0" koa-conditional-get "^2.0.0" koa-connect "^2.0.1" koa-csrf "^3.0.8" @@ -9510,10 +9510,10 @@ koa-compose@^4.1.0: resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== -koa-compress@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/koa-compress/-/koa-compress-3.0.0.tgz#3194059c215cbc24e59bbc84c2c7453a4c88564f" - integrity sha512-xol+LkNB1mozKJkB5Kj6nYXbJXhkLkZlXl9BsGBPjujVfZ8MsIXwU4GHRTT7TlSfUcl2DU3JtC+j6wOWcovfuQ== +koa-compress@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/koa-compress/-/koa-compress-3.1.0.tgz#00fb0af695dc4661c6de261a18da669626ea3ca1" + integrity sha512-0m24/yS/GbhWI+g9FqtvStY+yJwTObwoxOvPok6itVjRen7PBWkjsJ8pre76m+99YybXLKhOJ62mJ268qyBFMQ== dependencies: bytes "^3.0.0" compressible "^2.0.0"