diff --git a/src/UI/Components/Button.js b/src/UI/Components/Button.js new file mode 100644 index 00000000..d00a68bb --- /dev/null +++ b/src/UI/Components/Button.js @@ -0,0 +1,50 @@ +import { Component } from "../../util/Component.js"; + +export default class Button extends Component { + static PROPERTIES = { + label: { value: 'Test Label' }, + on_click: { value: null }, + enabled: { value: true }, + } + + static RENDER_MODE = Component.NO_SHADOW; + + static CSS = /*css*/` + button { + margin: 0; + color: hsl(220, 25%, 31%); + } + `; + + create_template ({ template }) { + $(template).html(/*html*/` + + `); + } + + on_ready ({ listen }) { + if ( this.get('on_click') ) { + const $button = $(this.dom_).find('button'); + $button.on('click', async () => { + $button.html(`circle anim`); + const on_click = this.get('on_click'); + await on_click(); + $button.html(this.get('label')); + }); + } + + listen('enabled', enabled => { + $(this.dom_).find('button').prop('disabled', ! enabled); + }); + } +} + +// TODO: This is necessary because files can be loaded from +// both `/src/UI` and `/UI` in the URL; we need to fix that +if ( ! window.__component_button ) { + window.__component_button = true; + + customElements.define('c-button', Button); +} diff --git a/src/UI/Components/ConfirmationsView.js b/src/UI/Components/ConfirmationsView.js new file mode 100644 index 00000000..cca3d3fe --- /dev/null +++ b/src/UI/Components/ConfirmationsView.js @@ -0,0 +1,67 @@ +import { Component } from "../../util/Component.js"; + +/** + * Display a list of checkboxes for the user to confirm. + */ +export default class ConfirmationsView extends Component { + static PROPERTIES = { + confirmations: { + description: 'The list of confirmations to display', + }, + confirmed: { + description: 'True iff all confirmations are checked', + }, + } + + static CSS = /*css*/` + .confirmations { + display: flex; + flex-direction: column; + } + .looks-good { + margin-top: 20px; + color: hsl(220, 25%, 31%); + font-size: 20px; + font-weight: 700; + display: none; + } + ` + + create_template ({ template }) { + $(template).html(/*html*/` +
+ ${ + this.get('confirmations').map((confirmation, index) => { + return /*html*/` +
+ + +
+ `; + }).join('') + } + Looks good! +
+ `); + } + + on_ready ({ listen }) { + // update `confirmed` property when checkboxes are checked + $(this.dom_).find('input').on('change', () => { + this.set('confirmed', $(this.dom_).find('input').toArray().every(input => input.checked)); + if ( this.get('confirmed') ) { + $(this.dom_).find('.looks-good').show(); + } else { + $(this.dom_).find('.looks-good').hide(); + } + }); + } +} + +// TODO: This is necessary because files can be loaded from +// both `/src/UI` and `/UI` in the URL; we need to fix that +if ( ! window.__component_confirmationsView ) { + window.__component_confirmationsView = true; + + customElements.define('c-confirmations-view', ConfirmationsView); +} diff --git a/src/UI/Settings/UITabSecurity.js b/src/UI/Settings/UITabSecurity.js index f1433903..a4191782 100644 --- a/src/UI/Settings/UITabSecurity.js +++ b/src/UI/Settings/UITabSecurity.js @@ -48,7 +48,14 @@ export default { init: ($el_window) => { $el_window.find('.enable-2fa').on('click', async function (e) { - UIWindow2FASetup(); + const { promise } = await UIWindow2FASetup(); + const tfa_was_enabled = await promise; + + if ( tfa_was_enabled ) { + $el_window.find('.enable-2fa').hide(); + $el_window.find('.disable-2fa').show(); + $el_window.find('.user-otp-state').text(i18n('two_factor_enabled')); + } return; diff --git a/src/UI/UIComponentWindow.js b/src/UI/UIComponentWindow.js index 100dc863..ad2a1b64 100644 --- a/src/UI/UIComponentWindow.js +++ b/src/UI/UIComponentWindow.js @@ -13,7 +13,7 @@ import Placeholder from "../util/Placeholder.js" export default async function UIComponentWindow (options) { const placeholder = Placeholder(); - await UIWindow({ + const win = await UIWindow({ ...options, body_content: placeholder.html, @@ -22,4 +22,6 @@ export default async function UIComponentWindow (options) { options.component.attach(placeholder); options.component.focus(); console.log('UIComponentWindow', options.component); + + return win; } diff --git a/src/UI/UIWindow2FASetup.js b/src/UI/UIWindow2FASetup.js index ce5c9dee..8871b855 100644 --- a/src/UI/UIWindow2FASetup.js +++ b/src/UI/UIWindow2FASetup.js @@ -19,7 +19,11 @@ */ +import TeePromise from "../util/TeePromise.js"; +import ValueHolder from "../util/ValueHolder.js"; +import Button from "./Components/Button.js"; import CodeEntryView from "./Components/CodeEntryView.js"; +import ConfirmationsView from "./Components/ConfirmationsView.js"; import Flexer from "./Components/Flexer.js"; import QRCodeView from "./Components/QRCode.js"; import RecoveryCodesView from "./Components/RecoveryCodesView.js"; @@ -31,6 +35,7 @@ import UIAlert from "./UIAlert.js"; import UIComponentWindow from "./UIComponentWindow.js"; const UIWindow2FASetup = async function UIWindow2FASetup () { + // FIRST REQUEST :: Generate the QR code and recovery codes const resp = await fetch(`${api_origin}/auth/configure-2fa/setup`, { method: 'POST', headers: { @@ -41,6 +46,7 @@ const UIWindow2FASetup = async function UIWindow2FASetup () { }); const data = await resp.json(); + // SECOND REQUEST :: Verify the code [first wizard screen] const check_code_ = async function check_code_ (value) { const resp = await fetch(`${api_origin}/auth/configure-2fa/test`, { method: 'POST', @@ -58,8 +64,29 @@ const UIWindow2FASetup = async function UIWindow2FASetup () { return data.ok; }; + // FINAL REQUEST :: Enable 2FA [second wizard screen] + const enable_2fa_ = async function check_code_ (value) { + const resp = await fetch(`${api_origin}/auth/configure-2fa/enable`, { + method: 'POST', + headers: { + Authorization: `Bearer ${puter.authToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + }); + + const data = await resp.json(); + + return data.ok; + }; + let stepper; let code_entry; + let win; + let done_enabled = new ValueHolder(false); + + const promise = new TeePromise(); + const component = new StepView({ _ref: me => stepper = me, @@ -118,17 +145,40 @@ const UIWindow2FASetup = async function UIWindow2FASetup () { symbol: '5', text: 'Confirm Recovery Codes', }), + new ConfirmationsView({ + confirmations: [ + 'I have copied the recovery codes', + ], + confirmed: done_enabled, + }), + new Button({ + enabled: done_enabled, + on_click: async () => { + await enable_2fa_(); + stepper.next(); + }, + }), ] }), ] }) ; - UIComponentWindow({ + stepper.values_['done'].sub(value => { + if ( ! value ) return; + $(win).close(); + console.log('WE GOT HERE') + promise.resolve(true); + }) + + win = await UIComponentWindow({ component, on_before_exit: async () => { - console.log('this was called?'); - return await UIAlert({ + // If stepper was exhausted, we can close the window + if ( stepper.get('done') ) return true; + + // Otherwise the user is trying to cancel the setup + const will_close = await UIAlert({ message: i18n('cancel_2fa_setup'), buttons: [ { @@ -142,6 +192,11 @@ const UIWindow2FASetup = async function UIWindow2FASetup () { }, ] }); + + if ( will_close ) { + promise.resolve(false); + return true; + } }, title: 'Instant Login!', @@ -176,6 +231,8 @@ const UIWindow2FASetup = async function UIWindow2FASetup () { padding: '20px', }, }); + + return { promise }; } export default UIWindow2FASetup;