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(``);
+ 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;