From 1b9e5a8dd367910aaa496d41183016a89b331186 Mon Sep 17 00:00:00 2001 From: Shaun Warman Date: Sat, 2 May 2020 02:47:34 -0500 Subject: [PATCH] chore: sync otp updates from lad.sh --- template/app/controllers/web/2fa/index.js | 3 - template/app/controllers/web/admin/users.js | 6 +- template/app/controllers/web/auth.js | 33 ++-- template/app/controllers/web/index.js | 4 +- template/app/controllers/web/my-account.js | 77 +------- .../web/{2fa/recovery.js => otp.js} | 138 ++++++++++++++- template/app/models/user.js | 20 +-- template/app/views/_nav.pug | 2 +- template/app/views/admin/users/index.pug | 4 +- template/app/views/admin/users/retrieve.pug | 6 +- template/app/views/my-account/security.pug | 164 ++---------------- template/app/views/otp/keys.pug | 41 +++++ .../{2fa/otp-login.pug => otp/login.pug} | 10 +- .../otp-recovery.pug => otp/recovery.pug} | 6 +- template/app/views/otp/setup.pug | 108 ++++++++++++ template/app/views/{2fa => otp}/verify.pug | 0 template/config/index.js | 8 +- template/config/meta.js | 4 +- template/config/phrases.js | 3 +- template/helpers/policies.js | 2 + template/locales/en.json | 3 +- template/locales/es.json | 3 +- template/locales/zh.json | 3 +- template/package.json | 8 +- template/routes/web/2fa.js | 19 -- template/routes/web/index.js | 4 +- template/routes/web/my-account.js | 4 +- template/routes/web/otp.js | 24 +++ template/yarn.lock | 50 +++--- test/snapshots/test.js.md | 13 +- test/snapshots/test.js.snap | Bin 2496 -> 2515 bytes 31 files changed, 431 insertions(+), 339 deletions(-) delete mode 100644 template/app/controllers/web/2fa/index.js rename template/app/controllers/web/{2fa/recovery.js => otp.js} (54%) create mode 100644 template/app/views/otp/keys.pug rename template/app/views/{2fa/otp-login.pug => otp/login.pug} (83%) rename template/app/views/{2fa/otp-recovery.pug => otp/recovery.pug} (75%) create mode 100644 template/app/views/otp/setup.pug rename template/app/views/{2fa => otp}/verify.pug (100%) delete mode 100644 template/routes/web/2fa.js create mode 100644 template/routes/web/otp.js diff --git a/template/app/controllers/web/2fa/index.js b/template/app/controllers/web/2fa/index.js deleted file mode 100644 index 19d4560c..00000000 --- a/template/app/controllers/web/2fa/index.js +++ /dev/null @@ -1,3 +0,0 @@ -const recovery = require('./recovery'); - -module.exports = { recovery }; diff --git a/template/app/controllers/web/admin/users.js b/template/app/controllers/web/admin/users.js index 9a6c4bb2..0039d9b5 100644 --- a/template/app/controllers/web/admin/users.js +++ b/template/app/controllers/web/admin/users.js @@ -40,12 +40,12 @@ async function update(ctx) { body[config.passport.fields.givenName]; user[config.passport.fields.familyName] = body[config.passport.fields.familyName]; - user[config.passport.fields.twoFactorEnabled] = - body[config.passport.fields.twoFactorEnabled]; + user[config.passport.fields.otpEnabled] = + body[config.passport.fields.otpEnabled]; user.email = body.email; user.group = body.group; - if (boolean(!body[config.passport.fields.twoFactorEnabled])) + if (boolean(!body[config.passport.fields.otpEnabled])) user[config.userFields.pendingRecovery] = false; await user.save(); diff --git a/template/app/controllers/web/auth.js b/template/app/controllers/web/auth.js index f7d9e66d..f9eb9248 100644 --- a/template/app/controllers/web/auth.js +++ b/template/app/controllers/web/auth.js @@ -87,6 +87,7 @@ async function homeOrDashboard(ctx) { } async function login(ctx, next) { + // eslint-disable-next-line complexity await passport.authenticate('local', async (err, user, info) => { if (err) throw err; @@ -113,14 +114,14 @@ async function login(ctx, next) { delete ctx.session.returnTo; } - let greeting = 'Good morning'; - if (moment().format('HH') >= 12 && moment().format('HH') <= 17) - greeting = 'Good afternoon'; - else if (moment().format('HH') >= 17) greeting = 'Good evening'; - if (user) { await ctx.login(user); + let greeting = 'Good morning'; + if (moment().format('HH') >= 12 && moment().format('HH') <= 17) + greeting = 'Good afternoon'; + else if (moment().format('HH') >= 17) greeting = 'Good evening'; + ctx.flash('custom', { title: `${ctx.request.t('Hello')} ${ctx.state.emoji('wave')}`, text: user[config.passport.fields.givenName] @@ -136,14 +137,14 @@ async function login(ctx, next) { const uri = authenticator.keyuri( user.email, 'lad.sh', - user[config.passport.fields.twoFactorToken] + user[config.passport.fields.otpToken] ); ctx.state.user.qrcode = await qrcode.toDataURL(uri); await ctx.state.user.save(); - if (user[config.passport.fields.twoFactorEnabled] && !ctx.session.otp) - redirectTo = `/${ctx.locale}/2fa/otp/login`; + if (user[config.passport.fields.otpEnabled] && !ctx.session.otp) + redirectTo = `/${ctx.locale}/otp/login`; if (ctx.accepts('json')) { ctx.body = { redirectTo }; @@ -154,6 +155,11 @@ async function login(ctx, next) { return; } + let greeting = 'Good morning'; + if (moment().format('HH') >= 12 && moment().format('HH') <= 17) + greeting = 'Good afternoon'; + else if (moment().format('HH') >= 17) greeting = 'Good evening'; + ctx.flash('custom', { title: `${ctx.request.t('Hello')} ${ctx.state.emoji('wave')}`, text: user[config.passport.fields.givenName] @@ -199,7 +205,7 @@ async function recoveryKey(ctx) { ctx.state.redirectTo = redirectTo; - let recoveryKeys = ctx.state.user[config.userFields.twoFactorRecoveryKeys]; + let recoveryKeys = ctx.state.user[config.userFields.otpRecoveryKeys]; // ensure recovery matches user list of keys if ( @@ -216,13 +222,13 @@ async function recoveryKey(ctx) { recoveryKeys = recoveryKeys.filter( key => key !== ctx.request.body.recovery_passcode ); - ctx.state.user[config.userFields.twoFactorRecoveryKeys] = recoveryKeys; + ctx.state.user[config.userFields.otpRecoveryKeys] = recoveryKeys; await ctx.state.user.save(); ctx.session.otp = 'totp-recovery'; // send the user a success message - const message = ctx.translate('TWO_FACTOR_RECOVERY_SUCCESS'); + const message = ctx.translate('OTP_RECOVERY_SUCCESS'); if (ctx.accepts('html')) { ctx.flash('success', message); @@ -243,7 +249,10 @@ async function register(ctx) { // register the user const count = await Users.countDocuments({ group: 'admin' }); - const query = { email: body.email, group: count === 0 ? 'admin' : 'user' }; + const query = { + email: body.email, + group: count === 0 ? 'admin' : 'user' + }; query[config.userFields.hasVerifiedEmail] = false; query[config.userFields.hasSetPassword] = true; const user = await Users.register(query, body.password); diff --git a/template/app/controllers/web/index.js b/template/app/controllers/web/index.js index 100795bd..bb1d43bf 100644 --- a/template/app/controllers/web/index.js +++ b/template/app/controllers/web/index.js @@ -8,7 +8,7 @@ const admin = require('./admin'); const auth = require('./auth'); const myAccount = require('./my-account'); const support = require('./support'); -const twoFactor = require('./2fa'); +const otp = require('./otp'); function breadcrumbs(ctx, next) { // return early if its not a pure path (e.g. ignore static assets) @@ -28,4 +28,4 @@ function breadcrumbs(ctx, next) { return next(); } -module.exports = { support, auth, admin, myAccount, breadcrumbs, twoFactor }; +module.exports = { support, auth, admin, myAccount, breadcrumbs, otp }; diff --git a/template/app/controllers/web/my-account.js b/template/app/controllers/web/my-account.js index 8509145d..dcd4fec0 100644 --- a/template/app/controllers/web/my-account.js +++ b/template/app/controllers/web/my-account.js @@ -1,9 +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'); const config = require('../../../config'); @@ -78,86 +75,18 @@ 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]; + const otpRecoveryKeys = ctx.state.user[config.userFields.otpRecoveryKeys]; ctx.attachment('recovery-keys.txt'); - ctx.body = twoFactorRecoveryKeys + ctx.body = otpRecoveryKeys .toString() .replace(/,/g, '\n') .replace(/"/g, ''); } -async function setup2fa(ctx) { - 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('json')) { - ctx.body = { reloadPage: true }; - } else { - ctx.redirect('back'); - } -} - module.exports = { update, recoveryKeys, - resetAPIToken, - security, - setup2fa + resetAPIToken }; diff --git a/template/app/controllers/web/2fa/recovery.js b/template/app/controllers/web/otp.js similarity index 54% rename from template/app/controllers/web/2fa/recovery.js rename to template/app/controllers/web/otp.js index 258d5478..702b887b 100644 --- a/template/app/controllers/web/2fa/recovery.js +++ b/template/app/controllers/web/otp.js @@ -1,12 +1,131 @@ +const cryptoRandomString = require('crypto-random-string'); +const isSANB = require('is-string-and-not-blank'); +const qrcode = require('qrcode'); const Boom = require('@hapi/boom'); +const bull = require('../../../bull'); +const config = require('../../../config'); +const { authenticator } = require('otplib'); const { boolean } = require('boolean'); -const bull = require('../../../../bull'); -const isSANB = require('is-string-and-not-blank'); -const config = require('../../../../config'); -const { Inquiries } = require('../../../models'); +const { Inquiries } = require('../../models'); + +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}/otp/setup`; + 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.otpToken + ] = authenticator.generateSecret(); + + // generate otp recovery keys list used for fallback + const recoveryKeys = new Array(16) + .fill() + .map(() => cryptoRandomString({ length: 10, characters: '1234567890' })); + + ctx.state.user[config.userFields.otpRecoveryKeys] = recoveryKeys; + ctx.state.user = await ctx.state.user.save(); + + await ctx.render('otp/keys'); +} + +async function renderSetup(ctx) { + ctx.state.otpTokenURI = authenticator.keyuri( + ctx.state.user.email, + process.env.WEB_HOST, + ctx.state.user[config.passport.fields.otpToken] + ); + ctx.state.qrcode = await qrcode.toDataURL(ctx.state.otpTokenURI); + + await ctx.render('otp/setup'); +} + +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.otpEnabled] = 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 setup(ctx) { + const redirectTo = `/${ctx.locale}/my-account/security`; + if (ctx.method === 'DELETE') { + ctx.state.user[config.passport.fields.otpEnabled] = false; + } else if ( + ctx.method === 'POST' && + ctx.state.user[config.passport.fields.otpToken] + ) { + const isValid = authenticator.verify({ + token: ctx.request.body.token, + secret: ctx.state.user[config.passport.fields.otpToken] + }); + + if (!isValid) + return ctx.throw(Boom.badRequest(ctx.translate('INVALID_OTP_PASSCODE'))); + + ctx.state.user[config.passport.fields.otpEnabled] = 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 }; +} -async function recover(ctx) { - let redirectTo = `/${ctx.locale}/2fa/recovery/verify`; +async function recovery(ctx) { + let redirectTo = `/${ctx.locale}/otp/recovery/verify`; if (ctx.session && ctx.session.returnTo) { redirectTo = ctx.session.returnTo; @@ -158,6 +277,11 @@ async function verify(ctx) { } module.exports = { - recover, + disable, + keys, + recovery, + renderKeys, + renderSetup, + setup, verify }; diff --git a/template/app/models/user.js b/template/app/models/user.js index b098ebac..7df07cd1 100644 --- a/template/app/models/user.js +++ b/template/app/models/user.js @@ -79,7 +79,7 @@ object[config.userFields.apiToken] = { index: true }; -object[config.userFields.twoFactorRecoveryKeys] = Array; +object[config.userFields.otpRecoveryKeys] = Array; // password reset object[config.userFields.resetTokenExpiresAt] = Date; @@ -147,12 +147,12 @@ object[fields.githubProfileID] = { object[fields.githubAccessToken] = String; object[fields.githubRefreshToken] = String; -object[fields.twoFactorEnabled] = { +object[fields.otpEnabled] = { type: Boolean, default: false }; -object[fields.twoFactorToken] = String; +object[fields.otpToken] = String; // shared field names with @ladjs/i18n and email-templates object[config.lastLocaleField] = { @@ -190,16 +190,16 @@ User.pre('validate', function(next) { ? `${this[fields.displayName]} <${this.email}>` : this.email; - // if two-factor authentication values no longer valid + // if otp authentication values no longer valid // then disable it completely if ( - !this[fields.twoFactorEnabled] || + !this[fields.otpEnabled] || !Array.isArray( - this[config.userFields.twoFactorRecoveryKeys] || - this[config.userFields.twoFactorRecoveryKeys].length === 0 + this[config.userFields.otpRecoveryKeys] || + this[config.userFields.otpRecoveryKeys].length === 0 ) ) { - this[fields.twoFactorEnabled] = false; + this[fields.otpEnabled] = false; } next(); @@ -311,8 +311,8 @@ User.plugin(mongooseCommonPlugin, { config.userFields.verificationPin, config.userFields.verificationPinHasExpired, config.userFields.welcomeEmailSentAt, - config.userFields.twoFactorRecoveryKeys, - fields.twoFactorToken + config.userFields.otpRecoveryKeys, + fields.otpToken ] }); User.plugin(passportLocalMongoose, config.passportLocalMongoose); diff --git a/template/app/views/_nav.pug b/template/app/views/_nav.pug index fcb416e1..7b50d71c 100644 --- a/template/app/views/_nav.pug +++ b/template/app/views/_nav.pug @@ -19,7 +19,7 @@ if !user && !registerOrLogin span(aria-hidden='true') × .modal-body +registerOrLogin(verb, true) -nav(class=['/register', '/login', config.verificationPath].includes(ctx.pathWithoutLocale) ? 'bg-white' : 'fixed-top bg-light border border-top-0 border-left-0 border-right-0').navbar.navbar-expand-md.navbar-light +nav(class=['/register', '/login', config.verifyRoute].includes(ctx.pathWithoutLocale) ? 'bg-white' : 'fixed-top bg-light border border-top-0 border-left-0 border-right-0').navbar.navbar-expand-md.navbar-light button.navbar-toggler.bg-light.hidden-lg-up(type="button", data-toggle="collapse", data-target="#navbar-header", aria-controls="navbar-header", aria-expanded="false", aria-label=t('Toggle navigation')) span.navbar-toggler-icon a.navbar-brand.d-inline-block.m-0.p-0(href=user ? l(config.passportCallbackOptions.successReturnToOrRedirect) : l()) diff --git a/template/app/views/admin/users/index.pug b/template/app/views/admin/users/index.pug index ab7ec397..09461804 100644 --- a/template/app/views/admin/users/index.pug +++ b/template/app/views/admin/users/index.pug @@ -23,7 +23,7 @@ block body th(scope='col')= t('Last IP') th(scope='col')= t('Last Locale') if boolean(process.env.AUTH_OTP_ENABLED) - th(scope='col')= t('2FA Enabled') + th(scope='col')= t('OTP Enabled') th(scope='col')= t('Actions') tbody each user in users @@ -38,7 +38,7 @@ block body td.align-middle: code= user[config.storeIPAddress.ip] td.align-middle: code= user[config.lastLocaleField] if boolean(process.env.AUTH_OTP_ENABLED) - td.align-middle= user[config.passport.fields.twoFactorEnabled] + td.align-middle= user[config.passport.fields.otpEnabled] td.align-middle .btn-group(role='group', aria-label=t('Actions')) a(href=l(`/admin/users/${user.id}`), data-toggle='tooltip', data-title=t('Edit')).btn.btn-secondary: i.fa.fa-fw.fa-edit diff --git a/template/app/views/admin/users/retrieve.pug b/template/app/views/admin/users/retrieve.pug index 10b65e08..7bc27a0d 100644 --- a/template/app/views/admin/users/retrieve.pug +++ b/template/app/views/admin/users/retrieve.pug @@ -26,10 +26,10 @@ block body option(value='user', selected=result.group === 'user') User option(value='admin', selected=result.group === 'admin') Admin label(for='input-group')= t('Group') - if boolean(process.env.AUTH_OTP_ENABLED) && result[config.passport.fields.twoFactorEnabled] + if boolean(process.env.AUTH_OTP_ENABLED) && result[config.passport.fields.otpEnabled] .form-check - input#two-factor-enabled(type='checkbox', name=config.passport.fields.twoFactorEnabled, value='true' checked) - label(for='input-two-factor-enabled')= t('Two-Factor Enabled') + input#otp-enabled(type='checkbox', name=config.passport.fields.otpEnabled, value='true' checked) + label(for='input-otp-enabled')= t('OTP Enabled') .card-footer.text-right button(type='reset').btn.btn-secondary= t('Reset') button(type='submit').btn.btn-primary= t('Save') diff --git a/template/app/views/my-account/security.pug b/template/app/views/my-account/security.pug index 31212a21..e1c19c02 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-otp(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('/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 @@ -151,18 +26,17 @@ block body .card.card-bg-light.mb-3 h4.card-header= t('Two-Factor Authentication') .card-body - if user[config.passport.fields.twoFactorEnabled] + if user[config.passport.fields.otpEnabled] h5= t('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. Download your recovery keys and put them in a safe place to use as a last resort.') 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-otp', 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('/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/app/views/otp/keys.pug b/template/app/views/otp/keys.pug new file mode 100644 index 00000000..9e1abfc4 --- /dev/null +++ b/template/app/views/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="otp-recovery-keys")= t('Recovery keys') + textarea(rows='4').form-control#otp-recovery-keys + = user[config.userFields.otpRecoveryKeys].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="#otp-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('/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/otp/login.pug similarity index 83% rename from template/app/views/2fa/otp-login.pug rename to template/app/views/otp/login.pug index 3e6b2492..6e7267bb 100644 --- a/template/app/views/2fa/otp-login.pug +++ b/template/app/views/otp/login.pug @@ -8,9 +8,9 @@ block body .card .card-body .text-center - h1.card-title.h4= t('Two-Factor Authentication Passcode') + h1.card-title.h4= t('OTP Passcode') p= t('Please enter the passcode from your authenticator application.') - form(action=l('/2fa/otp/login'), method="POST", autocomplete="off").ajax-form + form(action=l('/otp/login'), method="POST", autocomplete="off").ajax-form input(type="hidden", name="_csrf", value=ctx.csrf) .form-group.floating-label input#input-text.form-control.form-control-lg(type="text", autocomplete="off", name="passcode", placeholder=t('')) @@ -25,9 +25,9 @@ block body hr ul.list-inline li - a.card-link(href=l('/2fa/recovery/keys'))= t('Use recovery key') + a.card-link(href=l('/otp/recovery/keys'))= t('Use recovery key') li - a.card-link(href=l('/2fa/recovery/keys'), data-toggle='modal-anchor', data-target='#modal-domain')= t('Unable to use authentication app or recovery keys?') + a.card-link(href=l('/otp/recovery'), data-toggle='modal-anchor', data-target='#modal-domain')= t('Unable to use authentication app or recovery keys?') #modal-domain.modal.fade(tabindex='-1', role='dialog', aria-labelledby='modal-domain-title', aria-hidden='true') .modal-dialog(role='document') @@ -37,7 +37,7 @@ block body button(type='button', data-dismiss='modal', aria-label='Close').close span(aria-hidden='true') × .modal-body - form.ajax-form(action=l('/2fa/recovery'), method="POST") + form.ajax-form(action=l('/otp/recovery'), method="POST") p= t("If you can't access a trusted device or recovery codes you can request a reset of your security settings. For security reasons this can take 3-5 business days.") .container .row.row-cols-1.row-cols-lg diff --git a/template/app/views/2fa/otp-recovery.pug b/template/app/views/otp/recovery.pug similarity index 75% rename from template/app/views/2fa/otp-recovery.pug rename to template/app/views/otp/recovery.pug index 8f0f35aa..8355e0bf 100644 --- a/template/app/views/2fa/otp-recovery.pug +++ b/template/app/views/otp/recovery.pug @@ -8,9 +8,9 @@ block body .card .card-body .text-center - h1.card-title.h4= t('Two-Factor Authentication Recovery') - p= t('Please enter a two-factor recovery key to login.') - form(action=`${ctx.locale}/2fa/recovery/keys`, method="POST", autocomplete="off").ajax-form + h1.card-title.h4= t('OTP Recovery') + p= t('Please enter an OTP recovery key to login.') + form(action=`${ctx.locale}/otp/recovery/keys`, method="POST", autocomplete="off").ajax-form input(type="hidden", name="_csrf", value=ctx.csrf) .form-group.floating-label input#input-text.form-control.form-control-lg(type="text", autocomplete="off", name="recovery_passcode", placeholder=t('')) diff --git a/template/app/views/otp/setup.pug b/template/app/views/otp/setup.pug new file mode 100644 index 00000000..8ff79706 --- /dev/null +++ b/template/app/views/otp/setup.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('/otp/setup'), method='POST').ajax-form.confirm-prompt + input(type="hidden", name="_csrf", value=ctx.csrf) + label(for='otp-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='otp-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='otp-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='#otp-copy')= t('Can’t scan the QR code? Follow alternative steps') + #otp-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.otpToken]).form-control#otp-token + .input-group-append + button(type='button', data-toggle="clipboard", data-clipboard-target="#otp-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/2fa/verify.pug b/template/app/views/otp/verify.pug similarity index 100% rename from template/app/views/2fa/verify.pug rename to template/app/views/otp/verify.pug diff --git a/template/config/index.js b/template/config/index.js index 78df513b..ae91e510 100644 --- a/template/config/index.js +++ b/template/config/index.js @@ -99,7 +99,7 @@ const config = { userFields: { fullEmail: 'full_email', apiToken: 'api_token', - twoFactorRecoveryKeys: 'two_factor_recovery_keys', + otpRecoveryKeys: 'otp_recovery_keys', resetTokenExpiresAt: 'reset_token_expires_at', resetToken: 'reset_token', hasSetPassword: 'has_set_password', @@ -113,7 +113,7 @@ const config = { }, // dynamic otp routes - loginOtpRoute: '/2fa/otp/login', + loginOtpRoute: '/otp/login', // verification pin verifyRoute: '/verify', @@ -138,8 +138,8 @@ const config = { githubProfileID: 'github_profile_id', githubAccessToken: 'github_access_token', githubRefreshToken: 'github_refresh_token', - twoFactorToken: 'two_factor_token', - twoFactorEnabled: 'two_factor_enabled' + otpToken: 'otp_token', + otpEnabled: 'otp_enabled' }, google: { accessType: 'offline', diff --git a/template/config/meta.js b/template/config/meta.js index afc5fec0..7f8e38e9 100644 --- a/template/config/meta.js +++ b/template/config/meta.js @@ -57,9 +57,9 @@ module.exports = function(config) { 'Confirm your password reset token' ], '/auth': [`Auth ${lad}`, 'Authenticate yourself to log in'], - '/2fa': [ + '/otp': [ `Two Factor Auth ${lad}`, - 'Authenticate yourself with optional two factor auth to log in' + 'Authenticate yourself with optional OTP to log in' ], '/404': [ `Page not found ${lad}`, diff --git a/template/config/phrases.js b/template/config/phrases.js index 61cde666..4f9317a1 100644 --- a/template/config/phrases.js +++ b/template/config/phrases.js @@ -37,11 +37,12 @@ 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: 'We have sent you an email with a link to reset your password.', - TWO_FACTOR_RECOVERY_SUCCESS: + OTP_RECOVERY_SUCCESS: 'Recovery passcode successful. This passcode will no longer be valid.', REGISTERED: 'You have successfully registered.', REQUEST_OK: 'Your request was successfully completed.', diff --git a/template/helpers/policies.js b/template/helpers/policies.js index c2f2827d..535b695f 100644 --- a/template/helpers/policies.js +++ b/template/helpers/policies.js @@ -4,6 +4,7 @@ const { loginOtpRoute, verifyRoute, userFields, + passport, appName } = require('../config'); const { Users } = require('../app/models'); @@ -12,6 +13,7 @@ const policies = new Policies( { schemeName: appName, userFields, + passport, verifyRoute, loginOtpRoute }, diff --git a/template/locales/en.json b/template/locales/en.json index 0726c273..218e2c83 100644 --- a/template/locales/en.json +++ b/template/locales/en.json @@ -123,5 +123,6 @@ "Sign up with GitHub": "Sign up with GitHub", "or": "or", "Sign in with Google": "Sign in with Google", - "Sign in with GitHub": "Sign in with GitHub" + "Sign in with GitHub": "Sign in with GitHub", + "Invalid API token.": "Invalid API token." } \ No newline at end of file diff --git a/template/locales/es.json b/template/locales/es.json index 0726c273..218e2c83 100644 --- a/template/locales/es.json +++ b/template/locales/es.json @@ -123,5 +123,6 @@ "Sign up with GitHub": "Sign up with GitHub", "or": "or", "Sign in with Google": "Sign in with Google", - "Sign in with GitHub": "Sign in with GitHub" + "Sign in with GitHub": "Sign in with GitHub", + "Invalid API token.": "Invalid API token." } \ No newline at end of file diff --git a/template/locales/zh.json b/template/locales/zh.json index 0726c273..218e2c83 100644 --- a/template/locales/zh.json +++ b/template/locales/zh.json @@ -123,5 +123,6 @@ "Sign up with GitHub": "Sign up with GitHub", "or": "or", "Sign in with Google": "Sign in with Google", - "Sign in with GitHub": "Sign in with GitHub" + "Sign in with GitHub": "Sign in with GitHub", + "Invalid API token.": "Invalid API token." } \ No newline at end of file diff --git a/template/package.json b/template/package.json index 7682bc8c..4fead7b0 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", @@ -41,11 +41,11 @@ "@ladjs/i18n": "^3.0.4", "@ladjs/mongoose": "^2.0.1", "@ladjs/mongoose-error-messages": "^0.0.3", - "@ladjs/passport": "^1.0.0", - "@ladjs/policies": "^2.0.2", + "@ladjs/passport": "^2.0.0", + "@ladjs/policies": "^3.0.0", "@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 deleted file mode 100644 index 0c1e3808..00000000 --- a/template/routes/web/2fa.js +++ /dev/null @@ -1,19 +0,0 @@ -const Router = require('@koa/router'); -const render = require('koa-views-render'); - -const policies = require('../../helpers/policies'); -const web = require('../../app/controllers/web'); - -const router = new Router({ prefix: '/2fa' }); -router.use(policies.ensureLoggedIn); - -router - .get('/otp/login', render('2fa/otp-login')) - .post('/otp/login', web.auth.loginOtp) - .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')) - .post('/recovery/keys', web.auth.recoveryKey); - -module.exports = router; diff --git a/template/routes/web/index.js b/template/routes/web/index.js index 24121058..acff4782 100644 --- a/template/routes/web/index.js +++ b/template/routes/web/index.js @@ -9,7 +9,7 @@ const { web } = require('../../app/controllers'); const admin = require('./admin'); const auth = require('./auth'); const myAccount = require('./my-account'); -const twofactor = require('./2fa'); +const otp = require('./otp'); const router = new Router(); @@ -66,7 +66,7 @@ localeRouter localeRouter.use(myAccount.routes()); localeRouter.use(admin.routes()); -if (boolean(process.env.AUTH_OTP_ENABLED)) localeRouter.use(twofactor.routes()); +if (boolean(process.env.AUTH_OTP_ENABLED)) localeRouter.use(otp.routes()); router.use(auth.routes()); router.use(localeRouter.routes()); diff --git a/template/routes/web/my-account.js b/template/routes/web/my-account.js index 5f13a4a4..6caa8b3b 100644 --- a/template/routes/web/my-account.js +++ b/template/routes/web/my-account.js @@ -12,9 +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.post('/setup-2fa', web.myAccount.setup2fa); -router.delete('/setup-2fa', web.myAccount.setup2fa); +router.get('/security', render('my-account/security')); router.post('/recovery-keys', web.myAccount.recoveryKeys); module.exports = router; diff --git a/template/routes/web/otp.js b/template/routes/web/otp.js new file mode 100644 index 00000000..e193a986 --- /dev/null +++ b/template/routes/web/otp.js @@ -0,0 +1,24 @@ +const Router = require('@koa/router'); +const render = require('koa-views-render'); + +const policies = require('../../helpers/policies'); +const web = require('../../app/controllers/web'); + +const router = new Router({ prefix: '/otp' }); +router.use(policies.ensureLoggedIn); + +router + .get('/login', render('otp/login')) + .post('/login', web.auth.loginOtp) + .get('/keys', web.otp.renderKeys) + .post('/keys', web.otp.keys) + .get('/setup', web.otp.renderSetup) + .post('/setup', web.otp.setup) + .post('/disable', web.otp.disable) + .post('/recovery', web.otp.recovery) + .get('/recovery/verify', render('otp/verify')) + .post('/recovery/verify', web.otp.verify) + .get('/recovery/keys', render('otp/recovery')) + .post('/recovery/keys', web.auth.recoveryKey); + +module.exports = router; diff --git a/template/yarn.lock b/template/yarn.lock index b4977606..e6b27009 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" @@ -1166,10 +1166,10 @@ delay "^4.3.0" lodash "^4.17.15" -"@ladjs/passport@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@ladjs/passport/-/passport-1.0.0.tgz#924adf86326ad2075d106c27afad69d2e774f14b" - integrity sha512-kc+FYrYHCyYIq6AGNRn9M1mz9BMo3CNzdxQmCha6pv56aRy7ZOtjFpzxDMywvDUJTqe7zNdVbiiGdegNvZGjbQ== +"@ladjs/passport@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@ladjs/passport/-/passport-2.0.0.tgz#dfcc401d1349b077b84f4a082370d019db7f46d3" + integrity sha512-Sp6whAawHx9aE1N+Ew8skaJ/gRKg0tR75xynM1evunpmjWggz3+Zm21+ifQ9sgV+Ctrg7l+s2dK1Ogp9qVoB0Q== dependencies: boolean "^3.0.0" koa-passport "^4.1.3" @@ -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@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@ladjs/policies/-/policies-3.0.0.tgz#fd5f007409f7c7d0c50efa82f2b9d3ec7bcfc774" + integrity sha512-99NgCCr/EssQjbKEK7Yqzj8ZgcFwthOkod3OmhvYrt2ytHLT8s/a+Hd+iCwL8vQTs5WxDGk7Gl1AAEoWwfMAeA== 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" diff --git a/test/snapshots/test.js.md b/test/snapshots/test.js.md index d3547198..ffe84519 100644 --- a/test/snapshots/test.js.md +++ b/test/snapshots/test.js.md @@ -31,20 +31,16 @@ Generated by [AVA](https://ava.li). 'app/controllers/api/v1/log.js', 'app/controllers/api/v1/users.js', 'app/controllers/index.js', - 'app/controllers/web/2fa/index.js', - 'app/controllers/web/2fa/recovery.js', 'app/controllers/web/admin/index.js', 'app/controllers/web/admin/users.js', 'app/controllers/web/auth.js', 'app/controllers/web/index.js', 'app/controllers/web/my-account.js', + 'app/controllers/web/otp.js', 'app/controllers/web/support.js', 'app/models/index.js', 'app/models/inquiry.js', 'app/models/user.js', - 'app/views/2fa/otp-login.pug', - 'app/views/2fa/otp-recovery.pug', - 'app/views/2fa/verify.pug', 'app/views/404.pug', 'app/views/500.pug', 'app/views/_breadcrumbs.pug', @@ -63,6 +59,11 @@ Generated by [AVA](https://ava.li). 'app/views/layout.pug', 'app/views/my-account/index.pug', 'app/views/my-account/security.pug', + 'app/views/otp/keys.pug', + 'app/views/otp/login.pug', + 'app/views/otp/recovery.pug', + 'app/views/otp/setup.pug', + 'app/views/otp/verify.pug', 'app/views/privacy.pug', 'app/views/register-or-login.pug', 'app/views/reset-password.pug', @@ -150,11 +151,11 @@ Generated by [AVA](https://ava.li). 'routes/api/index.js', 'routes/api/v1/index.js', 'routes/index.js', - 'routes/web/2fa.js', 'routes/web/admin.js', 'routes/web/auth.js', 'routes/web/index.js', 'routes/web/my-account.js', + 'routes/web/otp.js', 'test/api/v1.js', 'test/config/utilities.js', 'test/global.js', diff --git a/test/snapshots/test.js.snap b/test/snapshots/test.js.snap index a66d5ab636d6bbcf48dc911d0a9d002e009340c1..42aead1debbab3b28f0c7ecc13073e2e4c340d27 100644 GIT binary patch literal 2515 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 literal 2496 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 K#tb4`9RL7iisdE%