Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions template/app/controllers/web/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 1 addition & 2 deletions template/app/controllers/web/otp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
2 changes: 1 addition & 1 deletion template/app/controllers/web/otp/keys.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion template/app/controllers/web/otp/recovery.js
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
4 changes: 2 additions & 2 deletions template/app/controllers/web/otp/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
124 changes: 0 additions & 124 deletions template/app/controllers/web/otp/verify.js

This file was deleted.

115 changes: 115 additions & 0 deletions template/app/views/otp/enable.pug
Original file line number Diff line number Diff line change
@@ -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('<strong>Step 1:</strong> Install and open an <a href="#" data-toggle="modal-anchor" data-target="#authenticator-apps-modal">authenticator app</a>.')
.form-group
label(for='otp-step-two')
!= t('<strong>Step 2:</strong> 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')
44 changes: 12 additions & 32 deletions template/app/views/otp/keys.pug
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Loading