diff --git a/packages/backend/src/routers/auth/configure-2fa.js b/packages/backend/src/routers/auth/configure-2fa.js index df506701..bc235c50 100644 --- a/packages/backend/src/routers/auth/configure-2fa.js +++ b/packages/backend/src/routers/auth/configure-2fa.js @@ -47,6 +47,17 @@ module.exports = eggspress('/auth/configure-2fa/:action', { return result; }; + // IMPORTANT: only use to verify the user's 2FA setup; + // this should never be used to verify the user's 2FA code + // for authentication purposes. + actions.test = async () => { + const user = req.user; + const svc_otp = x.get('services').get('otp'); + const code = req.body.code; + const delta = svc_otp.verify(user.username, user.otp_secret, code); + return { ok: delta !== null, delta }; + }; + actions.enable = async () => { await db.write( `UPDATE user SET otp_enabled = 1 WHERE uuid = ?`, diff --git a/src/UI/Components/CodeEntryView.js b/src/UI/Components/CodeEntryView.js index 48c590f7..3e647610 100644 --- a/src/UI/Components/CodeEntryView.js +++ b/src/UI/Components/CodeEntryView.js @@ -3,6 +3,7 @@ import { Component } from "../../util/Component.js"; export default class CodeEntryView extends Component { static PROPERTIES = { value: {}, + error: {}, is_checking_code: {}, } @@ -70,6 +71,11 @@ export default class CodeEntryView extends Component { on_ready ({ listen }) { let is_checking_code = false; + listen('error', (error) => { + if ( ! error ) return $(this.dom_).find('.error').hide(); + $(this.dom_).find('.error').text(error).show(); + }); + $(this.dom_).find('.digit-input').first().focus(); $(this.dom_).find('.code-confirm-btn').on('click submit', function(e){ diff --git a/src/UI/Components/RecoveryCodesView.js b/src/UI/Components/RecoveryCodesView.js new file mode 100644 index 00000000..1f636bed --- /dev/null +++ b/src/UI/Components/RecoveryCodesView.js @@ -0,0 +1,73 @@ +import { Component } from "../../util/Component.js"; + +export default class RecoveryCodesView extends Component { + static PROPERTIES = { + values: { + description: 'The recovery codes to display', + } + } + + static CSS = /*css*/` + .recovery-codes { + border: 1px solid #ccc; + padding: 20px; + margin: 20px auto; + width: 90%; + max-width: 600px; + background-color: #f9f9f9; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .recovery-codes h2 { + text-align: center; + font-size: 18px; + color: #333; + margin-bottom: 15px; + } + + .recovery-codes-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 10px; /* Adds space between grid items */ + padding: 0; + } + + .recovery-code { + background-color: #fff; + border: 1px solid #ddd; + padding: 10px; + text-align: center; + font-family: 'Courier New', Courier, monospace; + font-size: 12px; + letter-spacing: 1px; + } + ` + + + create_template ({ template }) { + $(template).html(` +