diff --git a/template/app/controllers/web/auth.js b/template/app/controllers/web/auth.js
index e2e9a314..ae5bb78b 100644
--- a/template/app/controllers/web/auth.js
+++ b/template/app/controllers/web/auth.js
@@ -13,6 +13,7 @@ const bull = require('../../../bull');
const Users = require('../../models/user');
const passport = require('../../../helpers/passport');
const config = require('../../../config');
+const { Inquiries } = require('../../models');
const sanitize = string =>
sanitizeHtml(string, {
@@ -447,7 +448,10 @@ async function verify(ctx) {
ctx.state.redirectTo = redirectTo;
- if (ctx.state.user[config.userFields.hasVerifiedEmail]) {
+ if (
+ ctx.state.user[config.userFields.hasVerifiedEmail] &&
+ !ctx.state.user[config.userFields.pendingRecovery]
+ ) {
const message = ctx.translate('EMAIL_ALREADY_VERIFIED');
if (ctx.accepts('html')) {
ctx.flash('success', message);
@@ -526,9 +530,39 @@ async function verify(ctx) {
ctx.state.user[config.userFields.hasVerifiedEmail] = true;
await ctx.state.user.save();
- // send the user a success message
- const message = ctx.translate('EMAIL_VERIFICATION_SUCCESS');
+ const pendingRecovery = ctx.state.user[config.userFields.pendingRecovery];
+ if (pendingRecovery) {
+ const body = {};
+ body.email = ctx.state.user.email;
+ body.message = ctx.translate('SUPPORT_REQUEST_MESSAGE');
+ body.is_email_only = true;
+ const inquiry = await Inquiries.create({
+ ...body,
+ ip: ctx.ip
+ });
+
+ ctx.logger.debug('created inquiry', inquiry);
+
+ const job = await bull.add('email', {
+ template: 'recovery',
+ message: {
+ to: ctx.state.user.email,
+ cc: config.email.message.from
+ },
+ locals: {
+ locale: ctx.locale,
+ inquiry
+ }
+ });
+
+ ctx.logger.info('added job', bull.getMeta({ job }));
+ }
+
+ const message = pendingRecovery
+ ? ctx.translate('PENDING_RECOVERY_VERIFICATION_SUCCESS')
+ : ctx.translate('EMAIL_VERIFICATION_SUCCESS');
+ redirectTo = pendingRecovery ? '/logout' : redirectTo;
if (ctx.accepts('html')) {
ctx.flash('success', message);
ctx.redirect(redirectTo);
diff --git a/template/app/controllers/web/otp/index.js b/template/app/controllers/web/otp/index.js
index ae9d0d82..8db3cabc 100644
--- a/template/app/controllers/web/otp/index.js
+++ b/template/app/controllers/web/otp/index.js
@@ -2,6 +2,5 @@ const disable = require('./disable');
const recovery = require('./recovery');
const setup = require('./setup');
const keys = require('./keys');
-const verify = require('./verify');
-module.exports = { disable, recovery, keys, setup, verify };
+module.exports = { disable, recovery, keys, setup };
diff --git a/template/app/controllers/web/otp/keys.js b/template/app/controllers/web/otp/keys.js
index 328fad14..c89832e9 100644
--- a/template/app/controllers/web/otp/keys.js
+++ b/template/app/controllers/web/otp/keys.js
@@ -1,7 +1,7 @@
async function keys(ctx) {
// this is like a migration, it will automatically add token + keys if needed
await ctx.state.user.save();
- await ctx.render('otp/keys');
+ await ctx.render('otp/setup');
}
module.exports = keys;
diff --git a/template/app/controllers/web/otp/recovery.js b/template/app/controllers/web/otp/recovery.js
index c6ccb12d..b162390f 100644
--- a/template/app/controllers/web/otp/recovery.js
+++ b/template/app/controllers/web/otp/recovery.js
@@ -1,7 +1,7 @@
const config = require('../../../../config');
async function recovery(ctx) {
- const redirectTo = `/${ctx.locale}/otp/recovery/verify`;
+ const redirectTo = `/${ctx.locale}/verify`;
ctx.state.redirectTo = redirectTo;
diff --git a/template/app/controllers/web/otp/setup.js b/template/app/controllers/web/otp/setup.js
index 820dd566..99a727f4 100644
--- a/template/app/controllers/web/otp/setup.js
+++ b/template/app/controllers/web/otp/setup.js
@@ -40,7 +40,7 @@ async function setup(ctx) {
ctx.state.user[config.passport.fields.otpToken]
);
ctx.state.qrcode = await qrcode.toDataURL(ctx.state.otpTokenURI, opts);
- return ctx.render('otp/setup');
+ return ctx.render('otp/enable');
}
ctx.state.user[config.passport.fields.otpEnabled] = true;
@@ -73,7 +73,7 @@ async function setup(ctx) {
ctx.state.user[config.passport.fields.otpToken]
);
ctx.state.qrcode = await qrcode.toDataURL(ctx.state.otpTokenURI, opts);
- return ctx.render('otp/setup');
+ return ctx.render('otp/enable');
}
module.exports = setup;
diff --git a/template/app/controllers/web/otp/verify.js b/template/app/controllers/web/otp/verify.js
deleted file mode 100644
index e50214c8..00000000
--- a/template/app/controllers/web/otp/verify.js
+++ /dev/null
@@ -1,124 +0,0 @@
-const Boom = require('@hapi/boom');
-const isSANB = require('is-string-and-not-blank');
-const { boolean } = require('boolean');
-
-const { Inquiries } = require('../../../models');
-const bull = require('../../../../bull');
-const config = require('../../../../config');
-
-// eslint-disable-next-line complexity
-async function verify(ctx) {
- let redirectTo = `/${ctx.locale}/login`;
-
- if (ctx.session && ctx.session.returnTo) {
- redirectTo = ctx.session.returnTo;
- delete ctx.session.returnTo;
- }
-
- ctx.state.redirectTo = redirectTo;
-
- // allow user to click a button to request a new email after 60 seconds
- // after their last attempt to get a verification email
- const resend = ctx.method === 'GET' && boolean(ctx.query.resend);
-
- if (
- !ctx.state.user[config.userFields.verificationPin] ||
- !ctx.state.user[config.userFields.verificationPinExpiresAt] ||
- ctx.state.user[config.userFields.verificationPinHasExpired] ||
- resend
- ) {
- try {
- ctx.state.user = await ctx.state.user.sendVerificationEmail(ctx);
- } catch (err) {
- // wrap with try/catch to prevent redirect looping
- // (even though the koa redirect loop package will help here)
- if (!err.isBoom) return ctx.throw(err);
- ctx.logger.warn(err);
- if (ctx.accepts('html')) {
- ctx.flash('warning', err.message);
- ctx.redirect(redirectTo);
- } else {
- ctx.body = { message: err.message };
- }
-
- return;
- }
-
- const message = ctx.translate(
- ctx.state.user[config.userFields.verificationPinHasExpired]
- ? 'EMAIL_VERIFICATION_EXPIRED'
- : 'EMAIL_VERIFICATION_SENT'
- );
-
- if (!ctx.accepts('html')) {
- ctx.body = { message };
- return;
- }
-
- ctx.flash('success', message);
- }
-
- // if it's a GET request then render the page
- if (ctx.method === 'GET' && !isSANB(ctx.query.pin))
- return ctx.render('verify');
-
- // if it's a POST request then ensure the user entered the 6 digit pin
- // otherwise if it's a GET request then use the ctx.query.pin
- let pin = '';
- if (ctx.method === 'GET') pin = ctx.query.pin;
- else pin = isSANB(ctx.request.body.pin) ? ctx.request.body.pin : '';
-
- // convert to digits only
- pin = pin.replace(/\D/g, '');
-
- // ensure pin matches up
- if (
- !ctx.state.user[config.userFields.verificationPin] ||
- pin !== ctx.state.user[config.userFields.verificationPin]
- )
- return ctx.throw(
- Boom.badRequest(ctx.translate('INVALID_VERIFICATION_PIN'))
- );
-
- try {
- const body = {};
- body.email = ctx.state.user.email;
- body.message = ctx.translate('SUPPORT_REQUEST_MESSAGE');
- body.is_email_only = true;
- const inquiry = await Inquiries.create({
- ...body,
- ip: ctx.ip
- });
-
- ctx.logger.debug('created inquiry', inquiry);
-
- const job = await bull.add('email', {
- template: 'recovery',
- message: {
- to: ctx.state.user.email,
- cc: config.email.message.from
- },
- locals: {
- locale: ctx.locale,
- inquiry
- }
- });
-
- ctx.logger.info('added job', bull.getMeta({ job }));
-
- const message = ctx.translate('PENDING_RECOVERY_VERIFICATION_SUCCESS');
- if (ctx.accepts('html')) {
- ctx.flash('success', message);
- ctx.redirect(redirectTo);
- } else {
- ctx.body = { message, redirectTo };
- }
- } catch (err) {
- ctx.logger.error(err);
- throw Boom.badRequest(ctx.translate('SUPPORT_REQUEST_ERROR'));
- }
-
- ctx.logout();
-}
-
-module.exports = verify;
diff --git a/template/app/views/otp/enable.pug b/template/app/views/otp/enable.pug
new file mode 100644
index 00000000..376aae18
--- /dev/null
+++ b/template/app/views/otp/enable.pug
@@ -0,0 +1,115 @@
+
+extends ../layout
+
+block body
+ #authenticator-apps-modal(tabindex='-1', role='dialog').modal.fade
+ .modal-dialog.modal-lg(role='document')
+ .modal-content
+ .modal-header
+ h1.h4.modal-title= t('Authenticator Apps')
+ button(type='button', data-dismiss='modal', aria-label='Close').close
+ span(aria-hidden='true') ×
+ .modal-body
+ .alert.alert-info= t('Our recommended apps are listed below. If you have feedback on this list, then please let us know.')
+ table.table
+ thead
+ tr
+ th(scope='col')= t('App')
+ th(scope='col')= t('Links')
+ th(scope='col').text-center= t('Open-Source')
+ tbody
+ tr
+ td.align-middle: a(href='https://freeotp.github.io/', rel='noopener', target='_blank') FreeOTP
+ td.align-middle
+ ul.list-inline.mb-0
+ li.list-inline-item.mb-1.mb-md-0
+ 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.mb-1.mb-md-0
+ 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-inline-item
+ a(href='https://f-droid.org/en/packages/org.fedorahosted.freeotp/', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary
+ i.fab.fa-android
+ = ' '
+ = t('F-Droid')
+ td.align-middle.text-center: i.fa.fa-check-circle.text-success
+ tr
+ td.align-middle: a(href='https://github.com/andOTP/andOTP', rel='noopener', target='_blank') andOTP
+ td.align-middle
+ ul.list-inline.mb-0
+ li.list-inline-item.mb-1.mb-md-0
+ 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')
+ li.list-inline-item
+ a(href='https://f-droid.org/en/packages/org.shadowice.flocke.andotp/', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary
+ i.fab.fa-android
+ = ' '
+ = t('F-Droid')
+ td.align-middle.text-center: i.fa.fa-check-circle.text-success
+ tr
+ td.align-middle: a(href='https://authy.com/', rel='noopener', target='_blank') Authy
+ td.align-middle
+ ul.list-inline.mb-0
+ li.list-inline-item.mb-1.mb-md-0
+ 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')
+ td.align-middle.text-center: i.fa.fa-times-circle.text-danger
+ tr
+ td.align-middle: a(href='https://support.google.com/accounts/answer/1066447', rel='noopener', target='_blank') Google Authenticator
+ td.align-middle
+ ul.list-inline.mb-0
+ li.list-inline-item.mb-1.mb-md-0
+ 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='https://apps.apple.com/us/app/google-authenticator/id388497605', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary
+ i.fab.fa-app-store-ios
+ = ' '
+ = t('App Store')
+ td.align-middle.text-center: i.fa.fa-times-circle.text-danger
+ .container.py-3
+ .row
+ .col-xs-12.col-sm-12.col-md-6.offset-md-3.col-lg-6.offset-lg-3
+ h1.my-3.py-3.text-center= t('Setup OTP')
+ form(action=ctx.path, method='POST').confirm-prompt
+ input(type="hidden", name="_csrf", value=ctx.csrf)
+ .form-group
+ label(for='otp-step-one')
+ != t('Step 1: Install and open an authenticator app.')
+ .form-group
+ label(for='otp-step-two')
+ != t('Step 2: Scan this QR code and enter its generated token:')
+ img(src=qrcode, width=250, height=250, alt="").d-block.my-3
+ .form-group.floating-label
+ input(type='text', name='token', required, placeholder=' ').form-control.form-control-lg#input-token
+ label(for='input-token') Verification Token
+ .form-group
+ a.btn.btn-link.text-center.text-primary(href='#' data-toggle='collapse' data-target='#otp-copy')
+ = t('Can’t scan the QR code? Configure with this code')
+ = ' '
+ i.fa.fa-angle-down
+ #otp-copy.collapse
+ .input-group.input-group-lg.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-dark
+ i.fa.fa-clipboard
+ = ' '
+ = t('Copy')
+ button(type='submit').btn.btn-lg.btn-block.btn-primary= t('Complete Setup')
diff --git a/template/app/views/otp/keys.pug b/template/app/views/otp/keys.pug
index a370b25d..cc6f3113 100644
--- a/template/app/views/otp/keys.pug
+++ b/template/app/views/otp/keys.pug
@@ -4,35 +4,15 @@ 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.text-center
- h1.my-3.py-3= t('Setup OTP')
- .alert.alert-warning(role='alert')
- i.fa.fa-exclamation-triangle
- = ' '
- = t('Download your emergency recovery keys below.')
- textarea(rows='5').form-control.text-monospace.text-center.rounded-bottom-0.border-dark#otp-recovery-keys
- each key, i in user[config.userFields.otpRecoveryKeys]
- = key
- if i !== user[config.userFields.otpRecoveryKeys].length - 1
- if i % 2
- = '\n'
- else
- = ' '
- form(action=l('/my-account/recovery-keys'), method='POST')
- input(type="hidden", name="_csrf", value=ctx.csrf)
- .d-flex.btn-group(role='group')
- button(type='submit').btn.btn-dark.rounded-top-0
- i.fa.fa-file-download
- = ' '
- = t('Download')
- button(type='button', data-toggle="clipboard", data-clipboard-text=user[config.userFields.otpRecoveryKeys].join('\r\n')).btn.btn-dark.rounded-top-0
- i.fa.fa-clipboard
- = ' '
- = t('Copy')
- form(action=ctx.path, method="POST", autocomplete="off", class=user[config.userFields.otpRecoveryKeys] ? '' : 'confirm-prompt')
- input(type="hidden", name="_csrf", value=ctx.csrf)
- if user[config.userFields.hasSetPassword]
- .form-group.floating-label.mt-4
- input#input-password.form-control.form-control-lg(type="password", autocomplete="off", name="password" required)
- label(for="input-password")= t('Confirm Password')
- button.btn.btn-primary.btn-lg.btn-block.mt-2(type="submit")= t('Continue')
+ .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')
+ p= t('Please enter an OTP recovery key to login.')
+ form(action=`${ctx.locale}/otp/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(''))
+ label(for="recovery_passcode")= t('Recovery Passcode')
+ button.btn.btn-primary.btn-block.btn-lg(type="submit")= t('Submit Passcode')
diff --git a/template/app/views/otp/login.pug b/template/app/views/otp/login.pug
index 7471cf49..c64a3d81 100644
--- a/template/app/views/otp/login.pug
+++ b/template/app/views/otp/login.pug
@@ -34,7 +34,7 @@ block body
.alert.alert-light.border.mt-3.text-center
= t('Having trouble?')
= ' '
- a(href=l('/otp/recovery/keys'))= t('Use a recovery key')
+ a(href=l('/otp/keys'))= t('Use a recovery key')
p.mb-0.text-center
small.text-muted
= t('Lose your recovery keys?')
diff --git a/template/app/views/otp/recovery.pug b/template/app/views/otp/recovery.pug
deleted file mode 100644
index 8355e0bf..00000000
--- a/template/app/views/otp/recovery.pug
+++ /dev/null
@@ -1,18 +0,0 @@
-
-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')
- 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(''))
- label(for="recovery_passcode")= t('Recovery Passcode')
- button.btn.btn-primary.btn-block.btn-lg(type="submit")= t('Submit Passcode')
diff --git a/template/app/views/otp/setup.pug b/template/app/views/otp/setup.pug
index 376aae18..a370b25d 100644
--- a/template/app/views/otp/setup.pug
+++ b/template/app/views/otp/setup.pug
@@ -2,114 +2,37 @@
extends ../layout
block body
- #authenticator-apps-modal(tabindex='-1', role='dialog').modal.fade
- .modal-dialog.modal-lg(role='document')
- .modal-content
- .modal-header
- h1.h4.modal-title= t('Authenticator Apps')
- button(type='button', data-dismiss='modal', aria-label='Close').close
- span(aria-hidden='true') ×
- .modal-body
- .alert.alert-info= t('Our recommended apps are listed below. If you have feedback on this list, then please let us know.')
- table.table
- thead
- tr
- th(scope='col')= t('App')
- th(scope='col')= t('Links')
- th(scope='col').text-center= t('Open-Source')
- tbody
- tr
- td.align-middle: a(href='https://freeotp.github.io/', rel='noopener', target='_blank') FreeOTP
- td.align-middle
- ul.list-inline.mb-0
- li.list-inline-item.mb-1.mb-md-0
- 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.mb-1.mb-md-0
- 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-inline-item
- a(href='https://f-droid.org/en/packages/org.fedorahosted.freeotp/', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary
- i.fab.fa-android
- = ' '
- = t('F-Droid')
- td.align-middle.text-center: i.fa.fa-check-circle.text-success
- tr
- td.align-middle: a(href='https://github.com/andOTP/andOTP', rel='noopener', target='_blank') andOTP
- td.align-middle
- ul.list-inline.mb-0
- li.list-inline-item.mb-1.mb-md-0
- 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')
- li.list-inline-item
- a(href='https://f-droid.org/en/packages/org.shadowice.flocke.andotp/', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary
- i.fab.fa-android
- = ' '
- = t('F-Droid')
- td.align-middle.text-center: i.fa.fa-check-circle.text-success
- tr
- td.align-middle: a(href='https://authy.com/', rel='noopener', target='_blank') Authy
- td.align-middle
- ul.list-inline.mb-0
- li.list-inline-item.mb-1.mb-md-0
- 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')
- td.align-middle.text-center: i.fa.fa-times-circle.text-danger
- tr
- td.align-middle: a(href='https://support.google.com/accounts/answer/1066447', rel='noopener', target='_blank') Google Authenticator
- td.align-middle
- ul.list-inline.mb-0
- li.list-inline-item.mb-1.mb-md-0
- 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='https://apps.apple.com/us/app/google-authenticator/id388497605', rel='noopener', target='_blank').btn.btn-sm.btn-outline-secondary
- i.fab.fa-app-store-ios
- = ' '
- = t('App Store')
- td.align-middle.text-center: i.fa.fa-times-circle.text-danger
.container.py-3
.row
- .col-xs-12.col-sm-12.col-md-6.offset-md-3.col-lg-6.offset-lg-3
- h1.my-3.py-3.text-center= t('Setup OTP')
- form(action=ctx.path, method='POST').confirm-prompt
+ .col-xs-12.col-sm-12.col-md-6.offset-md-3.col-lg-6.offset-lg-3.text-center
+ h1.my-3.py-3= t('Setup OTP')
+ .alert.alert-warning(role='alert')
+ i.fa.fa-exclamation-triangle
+ = ' '
+ = t('Download your emergency recovery keys below.')
+ textarea(rows='5').form-control.text-monospace.text-center.rounded-bottom-0.border-dark#otp-recovery-keys
+ each key, i in user[config.userFields.otpRecoveryKeys]
+ = key
+ if i !== user[config.userFields.otpRecoveryKeys].length - 1
+ if i % 2
+ = '\n'
+ else
+ = ' '
+ form(action=l('/my-account/recovery-keys'), method='POST')
input(type="hidden", name="_csrf", value=ctx.csrf)
- .form-group
- label(for='otp-step-one')
- != t('Step 1: Install and open an authenticator app.')
- .form-group
- label(for='otp-step-two')
- != t('Step 2: Scan this QR code and enter its generated token:')
- img(src=qrcode, width=250, height=250, alt="").d-block.my-3
- .form-group.floating-label
- input(type='text', name='token', required, placeholder=' ').form-control.form-control-lg#input-token
- label(for='input-token') Verification Token
- .form-group
- a.btn.btn-link.text-center.text-primary(href='#' data-toggle='collapse' data-target='#otp-copy')
- = t('Can’t scan the QR code? Configure with this code')
+ .d-flex.btn-group(role='group')
+ button(type='submit').btn.btn-dark.rounded-top-0
+ i.fa.fa-file-download
= ' '
- i.fa.fa-angle-down
- #otp-copy.collapse
- .input-group.input-group-lg.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-dark
- i.fa.fa-clipboard
- = ' '
- = t('Copy')
- button(type='submit').btn.btn-lg.btn-block.btn-primary= t('Complete Setup')
+ = t('Download')
+ button(type='button', data-toggle="clipboard", data-clipboard-text=user[config.userFields.otpRecoveryKeys].join('\r\n')).btn.btn-dark.rounded-top-0
+ i.fa.fa-clipboard
+ = ' '
+ = t('Copy')
+ form(action=ctx.path, method="POST", autocomplete="off", class=user[config.userFields.otpRecoveryKeys] ? '' : 'confirm-prompt')
+ input(type="hidden", name="_csrf", value=ctx.csrf)
+ if user[config.userFields.hasSetPassword]
+ .form-group.floating-label.mt-4
+ input#input-password.form-control.form-control-lg(type="password", autocomplete="off", name="password" required)
+ label(for="input-password")= t('Confirm Password')
+ button.btn.btn-primary.btn-lg.btn-block.mt-2(type="submit")= t('Continue')
diff --git a/template/routes/web/otp.js b/template/routes/web/otp.js
index 45655178..f1fef3fe 100644
--- a/template/routes/web/otp.js
+++ b/template/routes/web/otp.js
@@ -10,13 +10,11 @@ router.use(policies.ensureLoggedIn);
router
.get('/login', render('otp/login'))
.post('/login', web.auth.loginOtp)
- .get('/setup', render('otp/keys'))
+ .get('/setup', render('otp/setup'))
.post('/setup', web.otp.setup)
.post('/disable', web.otp.disable)
.post('/recovery', web.otp.recovery)
- .get('/recovery/verify', web.otp.verify)
- .post('/recovery/verify', web.otp.verify)
- .get('/recovery/keys', render('otp/recovery'))
- .post('/recovery/keys', web.auth.recoveryKey);
+ .get('/keys', render('otp/keys'))
+ .post('/keys', web.auth.recoveryKey);
module.exports = router;
diff --git a/test/snapshots/test.js.md b/test/snapshots/test.js.md
index 8f9faa71..790fb845 100644
--- a/test/snapshots/test.js.md
+++ b/test/snapshots/test.js.md
@@ -41,7 +41,6 @@ Generated by [AVA](https://ava.li).
'app/controllers/web/otp/keys.js',
'app/controllers/web/otp/recovery.js',
'app/controllers/web/otp/setup.js',
- 'app/controllers/web/otp/verify.js',
'app/controllers/web/support.js',
'app/models/index.js',
'app/models/inquiry.js',
@@ -64,9 +63,9 @@ 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/enable.pug',
'app/views/otp/keys.pug',
'app/views/otp/login.pug',
- 'app/views/otp/recovery.pug',
'app/views/otp/setup.pug',
'app/views/privacy.pug',
'app/views/register-or-login.pug',
diff --git a/test/snapshots/test.js.snap b/test/snapshots/test.js.snap
index 696d998a..71875c4e 100644
Binary files a/test/snapshots/test.js.snap and b/test/snapshots/test.js.snap differ