From 5b703c8db536c0f3bf794f87cfcceb220d9004af Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 6 Jun 2023 12:34:24 +0200 Subject: [PATCH] TOTP: improve login UI --- customize.dist/login.js | 38 +++++++++++++--------------- www/login/main.js | 46 ++++++++++++++++++++++++++++++++-- www/settings/app-settings.less | 7 ++++++ www/settings/inner.js | 2 +- 4 files changed, 69 insertions(+), 24 deletions(-) diff --git a/customize.dist/login.js b/customize.dist/login.js index 3da608230..967c9ce2f 100644 --- a/customize.dist/login.js +++ b/customize.dist/login.js @@ -135,7 +135,7 @@ define([ Exports.mergeAnonDrive = 1; }; - Exports.loginOrRegister = function (uname, passwd, isRegister, shouldImport, cb) { + Exports.loginOrRegister = function (uname, passwd, isRegister, shouldImport, onOTP, cb) { if (typeof(cb) !== 'function') { return; } // Usernames are all lowercase. No going back on this one @@ -173,23 +173,19 @@ define([ // determine where a block for your set of keys would be stored blockUrl = Block.getBlockUrl(res.opt.blockKeys); - var TOTP_prompt = function (cb) { - // XXX This should use nice UI elements integrated into - // the loading screen. window.prompt is here for prototyping only - var code = window.prompt('Enter TOTP'); - if (!code) { - return void cb("INVALID_TOTP"); - } - ServerCommand(res.opt.blockKeys.sign, { - command: 'TOTP_VALIDATE', - code: code, - // TODO optionally allow the user to specify a lifetime for this session? - // this will require a little bit of server work - // and more UI/UX: - // ie. just a simple "remember me" checkbox? - // allow them to specify a lifetime for the session? - // "log me out after one day"? - }, cb); + var TOTP_prompt = function (err, cb) { + onOTP(err, function (code) { + ServerCommand(res.opt.blockKeys.sign, { + command: 'TOTP_VALIDATE', + code: code, + // TODO optionally allow the user to specify a lifetime for this session? + // this will require a little bit of server work + // and more UI/UX: + // ie. just a simple "remember me" checkbox? + // allow them to specify a lifetime for the session? + // "log me out after one day"? + }, cb); + }); }; var done = waitFor(); @@ -248,7 +244,7 @@ define([ return void cb('TOTP_ATTEMPTS_EXHAUSTED'); } tries--; - TOTP_prompt(function (err, response) { + TOTP_prompt(tries !== 2, function (err, response) { // ask again until your number of tries are exhausted if (err) { console.error(err); @@ -558,7 +554,7 @@ define([ }; var hashing; - Exports.loginOrRegisterUI = function (uname, passwd, isRegister, shouldImport, testing, test) { + Exports.loginOrRegisterUI = function (uname, passwd, isRegister, shouldImport, onOTP, testing, test) { if (hashing) { return void console.log("hashing is already in progress"); } hashing = true; @@ -583,7 +579,7 @@ define([ // We need a setTimeout(cb, 0) otherwise the loading screen is only displayed // after hashing the password window.setTimeout(function () { - Exports.loginOrRegister(uname, passwd, isRegister, shouldImport, function (err, result) { + Exports.loginOrRegister(uname, passwd, isRegister, shouldImport, onOTP, function (err, result) { var proxy; if (result) { proxy = result.proxy; } diff --git a/www/login/main.js b/www/login/main.js index 1a27343fc..44649e534 100644 --- a/www/login/main.js +++ b/www/login/main.js @@ -6,10 +6,12 @@ define([ '/common/common-realtime.js', '/common/common-feedback.js', '/common/outer/local-store.js', + '/common/hyperscript.js', + '/customize/messages.js', //'/common/test.js', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', -], function ($, Cryptpad, Login, UI, Realtime, Feedback, LocalStore /*, Test */) { +], function ($, Cryptpad, Login, UI, Realtime, Feedback, LocalStore, h, Msg /*, Test */) { if (window.top !== window) { return; } $(function () { var $checkImport = $('#import-recent'); @@ -19,6 +21,11 @@ define([ return; } + Msg.settings_totp_code = "OTP code"; // XXX KEY ALREADY ADDED IN www/settings/inner.js + Msg.login_enter_totp = "This account is protected with MFA. Please enter your OTP code."; // XXX + Msg.login_invalid_otp = "Invalid OTP code"; + + /* Log in UI */ // deferred execution to avoid unnecessary asset loading var loginReady = function (cb) { @@ -44,12 +51,47 @@ define([ $('button.login').click(); }); + var onOTP = function (err, cb) { + var btn, input; + var error; + if (err) { + console.error(err); + error = h('p.cp-password-error', Msg.login_invalid_otp); + } + var block = h('div#cp-loading-password-prompt', [ + error, + h('p.cp-password-info', Msg.login_enter_totp), + h('p.cp-password-form', [ + input = h('input', { + placeholder: Msg.settings_totp_code, + autocomplete: 'off', + autocorrect: 'off', + autocapitalize: 'off', + spellcheck: false, + }), + btn = h('button.btn.btn-primary', Msg.ui_confirm) + ]) + ]); + var $input = $(input); + var $btn = $(btn).click(function () { + var val = $input.val(); + if (!val) { return void onOTP('INVALID_CODE', cb); } + cb(val); + }); + $(input).on('keydown', function (e) { + if (e.which !== 13) { return; } // enter + $btn.click(); + }); + UI.errorLoadingScreen(block, false, false); + }; + + //var test; $('button.login').click(function () { var shouldImport = $checkImport[0].checked; var uname = $uname.val(); var passwd = $passwd.val(); - Login.loginOrRegisterUI(uname, passwd, false, shouldImport, /*Test.testing */ false, function () { + Login.loginOrRegisterUI(uname, passwd, false, shouldImport, onOTP, /*Test.testing */ false, function () { /* if (test) { localStorage.clear(); diff --git a/www/settings/app-settings.less b/www/settings/app-settings.less index 6bd4874dd..c437d2dc3 100644 --- a/www/settings/app-settings.less +++ b/www/settings/app-settings.less @@ -103,6 +103,13 @@ margin-right: 5px; } } + .cp-settings-totp { + .cp-settings-qr { + img { + border: 10px solid white; + } + } + } } } diff --git a/www/settings/inner.js b/www/settings/inner.js index 9a599994b..58e6fc83b 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -951,7 +951,7 @@ define([ var uri = `otpauth://totp/${label}:${username}@${hostname}?secret=${secret}`; - var qr = h('div'); + var qr = h('div.cp-settings-qr'); var uriInput = UI.dialog.selectable(uri); updateQR(uri, qr);